├── .gitignore ├── README.md ├── assets └── final-result.png ├── babel.config.js ├── package.json ├── src ├── CustomRouteLayout.js ├── CustomRouteNoLayout.js ├── Layout.js ├── ThemedCustomRouteNoLayout.js ├── addUploadFeature.js ├── authProvider.js ├── comments │ ├── CommentCreate.js │ ├── CommentEdit.js │ ├── CommentList.js │ ├── CommentShow.js │ ├── PostPreview.js │ ├── PostQuickCreate.js │ ├── PostQuickCreateCancelButton.js │ ├── PostReferenceInput.js │ └── index.js ├── data.js ├── dataProvider.js ├── i18n │ ├── en.js │ ├── fr.js │ └── index.js ├── i18nProvider.js ├── index.html ├── index.js ├── posts │ ├── PostCreate.js │ ├── PostEdit.js │ ├── PostList.js │ ├── PostShow.js │ ├── PostTitle.js │ ├── ResetViewsButton.js │ ├── TagReferenceInput.js │ └── index.js ├── tags │ ├── TagCreate.js │ ├── TagEdit.js │ ├── TagList.js │ ├── TagShow.js │ └── index.js ├── theme.js ├── users │ ├── Aside.js │ ├── UserCreate.js │ ├── UserEdit.js │ ├── UserEditEmbedded.js │ ├── UserList.js │ ├── UserShow.js │ ├── UserTitle.js │ └── index.js └── validators.js ├── webpack.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Dependency directories 10 | node_modules/ 11 | 12 | # TypeScript v1 declaration files 13 | typings/ 14 | 15 | # TypeScript cache 16 | *.tsbuildinfo 17 | 18 | # Optional eslint cache 19 | .eslintcache 20 | 21 | # Yarn Integrity file 22 | .yarn-integrity 23 | 24 | # dotenv environment variables file 25 | .env 26 | .env.test 27 | 28 | # Build 29 | lib 30 | esm 31 | 32 | # Storybook Build 33 | public 34 | 35 | .envrc 36 | 37 | # Vercel / Now 38 | .vercel 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React-admin Tutorial - Changing The Look And Feel Of React-Admin Without JSX 2 | 3 | 4 | 5 | 6 | 13 | 14 |
publication 7 | Archived Repository 8 |
9 | The code of this repository was written to illustrate the blog post Changing The Look And Feel Of React-Admin Without JSX 10 |
11 | This code is not intended to be used in production, and is not maintained. 12 |
15 | 16 | ![Screenshot](./assets/final-result.png) 17 | 18 | ## How to run 19 | 20 | If you're running this app standalone: 21 | 22 | ```sh 23 | # install dependencies 24 | yarn 25 | # run the app in watch mode (reloads when a change is detected in the app code) 26 | yarn dev 27 | ``` 28 | 29 | If you're in the react-admin repository: 30 | 31 | ```sh 32 | # install the dependencies for the monorepo 33 | make install 34 | # run the app in extended watch mode (reloads when a change is detected in the app code and in the packages code) 35 | make run-simple 36 | ``` 37 | 38 | And then browse to [http://localhost:8080/](http://localhost:8080/). 39 | 40 | The credentials are **login/password** 41 | -------------------------------------------------------------------------------- /assets/final-result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Luwangel/react-admin-tutorials-build-your-own-theme/7e1030869ebeb4ce18e0209c9b11eafd23d2f133/assets/final-result.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const presets = [ 2 | [ 3 | '@babel/env', 4 | { 5 | targets: { 6 | edge: '17', 7 | firefox: '60', 8 | chrome: '67', 9 | safari: '11.1', 10 | }, 11 | useBuiltIns: 'usage', 12 | }, 13 | ], 14 | '@babel/preset-react', 15 | '@babel/preset-typescript', 16 | ]; 17 | 18 | const plugins = [ 19 | '@babel/plugin-proposal-class-properties', 20 | '@babel/plugin-proposal-object-rest-spread', 21 | '@babel/plugin-syntax-dynamic-import', 22 | ]; 23 | 24 | module.exports = { presets, plugins }; 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple", 3 | "private": true, 4 | "version": "3.0.0", 5 | "description": "", 6 | "main": "index.html", 7 | "scripts": { 8 | "dev": "./node_modules/.bin/webpack-dev-server --progress --color --hot --watch --mode development", 9 | "start": "./node_modules/.bin/webpack-dev-server --progress --color --hot --watch --mode development", 10 | "serve": "./node_modules/.bin/serve --listen 8080 ./dist", 11 | "build": "./node_modules/.bin/webpack-cli --color --mode development --hide-modules true" 12 | }, 13 | "author": "", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@babel/cli": "^7.1.2", 17 | "@babel/core": "^7.1.2", 18 | "@babel/plugin-proposal-class-properties": "^7.1.0", 19 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0", 20 | "@babel/plugin-syntax-dynamic-import": "^7.0.0", 21 | "@babel/preset-env": "^7.1.0", 22 | "@babel/preset-react": "^7.0.0", 23 | "@babel/preset-typescript": "^7.1.0", 24 | "babel-loader": "^8.0.4", 25 | "hard-source-webpack-plugin": "^0.11.2", 26 | "html-loader": "~0.5.5", 27 | "html-webpack-plugin": "~3.2.0", 28 | "ignore-not-found-export-plugin": "^1.0.1", 29 | "serve": "~9.1.0", 30 | "style-loader": "~0.20.3", 31 | "wait-on": "^3.2.0", 32 | "webpack": "~4.5.0", 33 | "webpack-bundle-analyzer": "^3.3.2", 34 | "webpack-cli": "~2.0.13", 35 | "webpack-dev-server": "~3.1.11" 36 | }, 37 | "dependencies": { 38 | "@babel/polyfill": "^7.0.0", 39 | "@material-ui/core": "^4.10.0", 40 | "@material-ui/icons": "^4.9.1", 41 | "ra-data-fakerest": "^3.0.0", 42 | "ra-i18n-polyglot": "^3.0.0", 43 | "ra-input-rich-text": "^3.0.0", 44 | "ra-language-english": "^3.0.0", 45 | "ra-language-french": "^3.0.0", 46 | "react": "^16.9.0", 47 | "react-admin": "^3.0.0", 48 | "react-dom": "^16.9.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/CustomRouteLayout.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useGetList, useAuthenticated, Title } from 'react-admin'; 3 | 4 | const CustomRouteLayout = () => { 5 | useAuthenticated(); 6 | const { ids, data, total, loaded } = useGetList( 7 | 'posts', 8 | { page: 1, perPage: 10 }, 9 | { field: 'published_at', order: 'DESC' } 10 | ); 11 | 12 | return loaded ? ( 13 |
14 | 15 | <h1>Posts</h1> 16 | <p> 17 | Found <span className="total">{total}</span> posts ! 18 | </p> 19 | <ul> 20 | {ids.map(id => ( 21 | <li key={id}>{data[id].title}</li> 22 | ))} 23 | </ul> 24 | </div> 25 | ) : null; 26 | }; 27 | 28 | export default CustomRouteLayout; 29 | -------------------------------------------------------------------------------- /src/CustomRouteNoLayout.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { useEffect } from "react"; 3 | import { useSelector, useDispatch } from "react-redux"; 4 | import { crudGetList } from "react-admin"; 5 | import { makeStyles, Typography } from "@material-ui/core"; 6 | import { useTheme } from "@material-ui/core/styles"; 7 | 8 | const CustomRouteNoLayout = (props) => { 9 | const classes = useStyles(props); 10 | const dispatch = useDispatch(); 11 | 12 | const loaded = useSelector( 13 | (state) => 14 | state.admin.resources.posts && state.admin.resources.posts.list.total > 0 15 | ); 16 | 17 | const total = useSelector((state) => 18 | state.admin.resources.posts ? state.admin.resources.posts.list.total : 0 19 | ); 20 | 21 | useEffect(() => { 22 | dispatch( 23 | crudGetList( 24 | "posts", 25 | { page: 0, perPage: 10 }, 26 | { field: "id", order: "ASC" } 27 | ) 28 | ); 29 | }, [dispatch]); 30 | 31 | return ( 32 | <div className={classes.root}> 33 | <Typography className={classes.title} variant="h2" component="h1"> 34 | Posts 35 | </Typography> 36 | {!loaded && ( 37 | <Typography className="app-loader" variant="body1"> 38 | Loading... 39 | </Typography> 40 | )} 41 | {loaded && ( 42 | <Typography variant="body1"> 43 | Found <span className="total">{total}</span> posts ! 44 | </Typography> 45 | )} 46 | </div> 47 | ); 48 | }; 49 | 50 | const useStyles = makeStyles((theme) => ({ 51 | root: { 52 | padding: theme.spacing(4), 53 | }, 54 | title: { 55 | color: theme.palette.secondary.main, 56 | }, 57 | })); 58 | 59 | export default CustomRouteNoLayout; 60 | -------------------------------------------------------------------------------- /src/Layout.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { forwardRef } from 'react'; 3 | import { Layout, AppBar, UserMenu, useLocale, useSetLocale } from 'react-admin'; 4 | import { makeStyles, MenuItem, ListItemIcon } from '@material-ui/core'; 5 | import Language from '@material-ui/icons/Language'; 6 | 7 | const useStyles = makeStyles(theme => ({ 8 | menuItem: { 9 | color: theme.palette.text.secondary, 10 | }, 11 | icon: { minWidth: theme.spacing(5) }, 12 | })); 13 | 14 | const SwitchLanguage = forwardRef((props, ref) => { 15 | const locale = useLocale(); 16 | const setLocale = useSetLocale(); 17 | const classes = useStyles(); 18 | return ( 19 | <MenuItem 20 | ref={ref} 21 | className={classes.menuItem} 22 | onClick={() => { 23 | setLocale(locale === 'en' ? 'fr' : 'en'); 24 | props.onClick(); 25 | }} 26 | > 27 | <ListItemIcon className={classes.icon}> 28 | <Language /> 29 | </ListItemIcon> 30 | Switch Language 31 | </MenuItem> 32 | ); 33 | }); 34 | 35 | const MyUserMenu = props => ( 36 | <UserMenu {...props}> 37 | <SwitchLanguage /> 38 | </UserMenu> 39 | ); 40 | 41 | const MyAppBar = props => <AppBar {...props} userMenu={<MyUserMenu />} />; 42 | 43 | export default props => <Layout {...props} appBar={MyAppBar} />; 44 | -------------------------------------------------------------------------------- /src/ThemedCustomRouteNoLayout.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ThemeProvider } from "@material-ui/core/styles"; 3 | 4 | import { theme } from "./theme"; 5 | import CustomRouteNoLayout from "./CustomRouteNoLayout"; 6 | 7 | const ThemedCustomRouteNoLayout = (props) => { 8 | return ( 9 | <ThemeProvider theme={theme}> 10 | <CustomRouteNoLayout {...props} /> 11 | </ThemeProvider> 12 | ); 13 | }; 14 | 15 | export default ThemedCustomRouteNoLayout; 16 | -------------------------------------------------------------------------------- /src/addUploadFeature.js: -------------------------------------------------------------------------------- 1 | /** 2 | * For posts update only, convert uploaded image in base 64 and attach it to 3 | * the `picture` sent property, with `src` and `title` attributes. 4 | */ 5 | const addUploadCapabilities = dataProvider => ({ 6 | ...dataProvider, 7 | update: (resource, params) => { 8 | if (resource !== 'posts' || !params.data.pictures) { 9 | // fallback to the default implementation 10 | return dataProvider.update(resource, params); 11 | } 12 | // The posts edition form uses a file upload widget for the pictures field. 13 | // Freshly dropped pictures are File objects 14 | // and must be converted to base64 strings 15 | const newPictures = params.data.pictures.filter( 16 | p => p.rawFile instanceof File 17 | ); 18 | const formerPictures = params.data.pictures.filter( 19 | p => !(p.rawFile instanceof File) 20 | ); 21 | 22 | return Promise.all(newPictures.map(convertFileToBase64)) 23 | .then(base64Pictures => 24 | base64Pictures.map(picture64 => ({ 25 | src: picture64, 26 | title: `${params.data.title}`, 27 | })) 28 | ) 29 | .then(transformedNewPictures => 30 | dataProvider.update(resource, { 31 | ...params, 32 | data: { 33 | ...params.data, 34 | pictures: [ 35 | ...transformedNewPictures, 36 | ...formerPictures, 37 | ], 38 | }, 39 | }) 40 | ); 41 | }, 42 | }); 43 | 44 | /** 45 | * Convert a `File` object returned by the upload input into a base 64 string. 46 | * That's not the most optimized way to store images in production, but it's 47 | * enough to illustrate the idea of data provider decoration. 48 | */ 49 | const convertFileToBase64 = file => 50 | new Promise((resolve, reject) => { 51 | const reader = new FileReader(); 52 | reader.readAsDataURL(file.rawFile); 53 | 54 | reader.onload = () => resolve(reader.result); 55 | reader.onerror = reject; 56 | }); 57 | 58 | export default addUploadCapabilities; 59 | -------------------------------------------------------------------------------- /src/authProvider.js: -------------------------------------------------------------------------------- 1 | // Authenticatd by default 2 | export default { 3 | login: ({ username, password }) => { 4 | if (username === 'login' && password === 'password') { 5 | localStorage.removeItem('not_authenticated'); 6 | localStorage.removeItem('role'); 7 | return Promise.resolve(); 8 | } 9 | if (username === 'user' && password === 'password') { 10 | localStorage.setItem('role', 'user'); 11 | localStorage.removeItem('not_authenticated'); 12 | return Promise.resolve(); 13 | } 14 | if (username === 'admin' && password === 'password') { 15 | localStorage.setItem('role', 'admin'); 16 | localStorage.removeItem('not_authenticated'); 17 | return Promise.resolve(); 18 | } 19 | localStorage.setItem('not_authenticated', true); 20 | return Promise.reject(); 21 | }, 22 | logout: () => { 23 | localStorage.setItem('not_authenticated', true); 24 | localStorage.removeItem('role'); 25 | return Promise.resolve(); 26 | }, 27 | checkError: ({ status }) => { 28 | return status === 401 || status === 403 29 | ? Promise.reject() 30 | : Promise.resolve(); 31 | }, 32 | checkAuth: () => { 33 | return localStorage.getItem('not_authenticated') 34 | ? Promise.reject() 35 | : Promise.resolve(); 36 | }, 37 | getPermissions: () => { 38 | const role = localStorage.getItem('role'); 39 | return Promise.resolve(role); 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /src/comments/CommentCreate.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { 4 | Create, 5 | DateInput, 6 | TextInput, 7 | SimpleForm, 8 | required, 9 | minLength, 10 | } from 'react-admin'; // eslint-disable-line import/no-unresolved 11 | import PostReferenceInput from './PostReferenceInput'; 12 | 13 | const now = new Date(); 14 | const defaultSort = { field: 'title', order: 'ASC' }; 15 | 16 | const CommentCreate = props => ( 17 | <Create {...props}> 18 | <SimpleForm redirect={false}> 19 | <PostReferenceInput 20 | source="post_id" 21 | reference="posts" 22 | allowEmpty 23 | validate={required()} 24 | perPage={10000} 25 | sort={defaultSort} 26 | /> 27 | <TextInput 28 | source="author.name" 29 | validate={minLength(10)} 30 | fullWidth 31 | /> 32 | <DateInput source="created_at" defaultValue={now} /> 33 | <TextInput source="body" fullWidth={true} multiline={true} /> 34 | </SimpleForm> 35 | </Create> 36 | ); 37 | 38 | export default CommentCreate; 39 | -------------------------------------------------------------------------------- /src/comments/CommentEdit.js: -------------------------------------------------------------------------------- 1 | import Card from '@material-ui/core/Card'; 2 | import Typography from '@material-ui/core/Typography'; 3 | import { makeStyles } from '@material-ui/core/styles'; 4 | import * as React from 'react'; 5 | import { 6 | AutocompleteInput, 7 | DateInput, 8 | EditActions, 9 | useEditController, 10 | Link, 11 | ReferenceInput, 12 | SimpleForm, 13 | TextInput, 14 | Title, 15 | minLength, 16 | } from 'react-admin'; // eslint-disable-line import/no-unresolved 17 | 18 | const LinkToRelatedPost = ({ record }) => ( 19 | <Link to={`/posts/${record.post_id}`}> 20 | <Typography variant="caption" color="inherit" align="right"> 21 | See related post 22 | </Typography> 23 | </Link> 24 | ); 25 | 26 | const useEditStyles = makeStyles({ 27 | actions: { 28 | float: 'right', 29 | }, 30 | card: { 31 | marginTop: '1em', 32 | maxWidth: '30em', 33 | }, 34 | }); 35 | 36 | const OptionRenderer = ({ record }) => ( 37 | <span> 38 | {record.title} - {record.id} 39 | </span> 40 | ); 41 | 42 | const inputText = record => `${record.title} - ${record.id}`; 43 | 44 | const CommentEdit = props => { 45 | const classes = useEditStyles(); 46 | const { 47 | resource, 48 | record, 49 | redirect, 50 | save, 51 | basePath, 52 | version, 53 | } = useEditController(props); 54 | return ( 55 | <div className="edit-page"> 56 | <Title defaultTitle={`Comment #${record ? record.id : ''}`} /> 57 | <div className={classes.actions}> 58 | <EditActions 59 | basePath={basePath} 60 | resource={resource} 61 | data={record} 62 | hasShow 63 | hasList 64 | /> 65 | </div> 66 | <Card className={classes.card}> 67 | {record && ( 68 | <SimpleForm 69 | basePath={basePath} 70 | redirect={redirect} 71 | resource={resource} 72 | record={record} 73 | save={save} 74 | version={version} 75 | > 76 | <TextInput disabled source="id" fullWidth /> 77 | <ReferenceInput 78 | source="post_id" 79 | reference="posts" 80 | perPage={15} 81 | sort={{ field: 'title', order: 'ASC' }} 82 | fullWidth 83 | > 84 | <AutocompleteInput 85 | matchSuggestion={(filterValue, suggestion) => 86 | true 87 | } 88 | optionText={<OptionRenderer />} 89 | inputText={inputText} 90 | options={{ fullWidth: true }} 91 | /> 92 | </ReferenceInput> 93 | 94 | <LinkToRelatedPost /> 95 | <TextInput 96 | source="author.name" 97 | validate={minLength(10)} 98 | fullWidth 99 | /> 100 | <DateInput source="created_at" fullWidth /> 101 | <TextInput 102 | source="body" 103 | validate={minLength(10)} 104 | fullWidth={true} 105 | multiline={true} 106 | /> 107 | </SimpleForm> 108 | )} 109 | </Card> 110 | </div> 111 | ); 112 | }; 113 | 114 | export default CommentEdit; 115 | -------------------------------------------------------------------------------- /src/comments/CommentList.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import ChevronLeft from '@material-ui/icons/ChevronLeft'; 3 | import ChevronRight from '@material-ui/icons/ChevronRight'; 4 | import PersonIcon from '@material-ui/icons/Person'; 5 | import { 6 | Avatar, 7 | Button, 8 | Card, 9 | CardActions, 10 | CardContent, 11 | CardHeader, 12 | Grid, 13 | Toolbar, 14 | useMediaQuery, 15 | makeStyles, 16 | } from '@material-ui/core'; 17 | import jsonExport from 'jsonexport/dist'; 18 | import { 19 | DateField, 20 | EditButton, 21 | Filter, 22 | List, 23 | PaginationLimit, 24 | ReferenceField, 25 | ReferenceInput, 26 | SearchInput, 27 | SelectInput, 28 | ShowButton, 29 | SimpleList, 30 | TextField, 31 | downloadCSV, 32 | useTranslate, 33 | } from 'react-admin'; // eslint-disable-line import/no-unresolved 34 | 35 | const CommentFilter = props => ( 36 | <Filter {...props}> 37 | <SearchInput source="q" alwaysOn /> 38 | <ReferenceInput source="post_id" reference="posts"> 39 | <SelectInput optionText="title" /> 40 | </ReferenceInput> 41 | </Filter> 42 | ); 43 | 44 | const exporter = (records, fetchRelatedRecords) => 45 | fetchRelatedRecords(records, 'post_id', 'posts').then(posts => { 46 | const data = records.map(record => { 47 | const { author, ...recordForExport } = record; // omit author 48 | recordForExport.author_name = author.name; 49 | recordForExport.post_title = posts[record.post_id].title; 50 | return recordForExport; 51 | }); 52 | const headers = [ 53 | 'id', 54 | 'author_name', 55 | 'post_id', 56 | 'post_title', 57 | 'created_at', 58 | 'body', 59 | ]; 60 | 61 | jsonExport(data, { headers }, (error, csv) => { 62 | if (error) { 63 | console.error(error); 64 | } 65 | downloadCSV(csv, 'comments'); 66 | }); 67 | }); 68 | 69 | const CommentPagination = ({ loading, ids, page, perPage, total, setPage }) => { 70 | const translate = useTranslate(); 71 | const nbPages = Math.ceil(total / perPage) || 1; 72 | if (!loading && (total === 0 || (ids && !ids.length))) { 73 | return <PaginationLimit total={total} page={page} ids={ids} />; 74 | } 75 | 76 | return ( 77 | nbPages > 1 && ( 78 | <Toolbar> 79 | {page > 1 && ( 80 | <Button 81 | color="primary" 82 | key="prev" 83 | onClick={() => setPage(page - 1)} 84 | > 85 | <ChevronLeft /> 86 |   87 | {translate('ra.navigation.prev')} 88 | </Button> 89 | )} 90 | {page !== nbPages && ( 91 | <Button 92 | color="primary" 93 | key="next" 94 | onClick={() => setPage(page + 1)} 95 | > 96 | {translate('ra.navigation.next')}  97 | <ChevronRight /> 98 | </Button> 99 | )} 100 | </Toolbar> 101 | ) 102 | ); 103 | }; 104 | 105 | const useListStyles = makeStyles(theme => ({ 106 | card: { 107 | height: '100%', 108 | display: 'flex', 109 | flexDirection: 'column', 110 | }, 111 | cardContent: theme.typography.body1, 112 | cardLink: { 113 | ...theme.typography.body1, 114 | flexGrow: 1, 115 | }, 116 | cardLinkLink: { 117 | display: 'inline', 118 | }, 119 | cardActions: { 120 | justifyContent: 'flex-end', 121 | }, 122 | })); 123 | 124 | const CommentGrid = ({ ids, data, basePath }) => { 125 | const translate = useTranslate(); 126 | const classes = useListStyles(); 127 | 128 | return ( 129 | <Grid spacing={2} container> 130 | {ids.map(id => ( 131 | <Grid item key={id} sm={12} md={6} lg={4}> 132 | <Card className={classes.card}> 133 | <CardHeader 134 | className="comment" 135 | title={ 136 | <TextField 137 | record={data[id]} 138 | source="author.name" 139 | /> 140 | } 141 | subheader={ 142 | <DateField 143 | record={data[id]} 144 | source="created_at" 145 | /> 146 | } 147 | avatar={ 148 | <Avatar> 149 | <PersonIcon /> 150 | </Avatar> 151 | } 152 | /> 153 | <CardContent className={classes.cardContent}> 154 | <TextField record={data[id]} source="body" /> 155 | </CardContent> 156 | <CardContent className={classes.cardLink}> 157 | {translate('comment.list.about')}  158 | <ReferenceField 159 | resource="comments" 160 | record={data[id]} 161 | source="post_id" 162 | reference="posts" 163 | basePath={basePath} 164 | > 165 | <TextField 166 | source="title" 167 | className={classes.cardLinkLink} 168 | /> 169 | </ReferenceField> 170 | </CardContent> 171 | <CardActions className={classes.cardActions}> 172 | <EditButton 173 | resource="posts" 174 | basePath={basePath} 175 | record={data[id]} 176 | /> 177 | <ShowButton 178 | resource="posts" 179 | basePath={basePath} 180 | record={data[id]} 181 | /> 182 | </CardActions> 183 | </Card> 184 | </Grid> 185 | ))} 186 | </Grid> 187 | ); 188 | }; 189 | 190 | CommentGrid.defaultProps = { 191 | data: {}, 192 | ids: [], 193 | }; 194 | 195 | const CommentMobileList = props => ( 196 | <SimpleList 197 | primaryText={record => record.author.name} 198 | secondaryText={record => record.body} 199 | tertiaryText={record => 200 | new Date(record.created_at).toLocaleDateString() 201 | } 202 | leftAvatar={() => <PersonIcon />} 203 | {...props} 204 | /> 205 | ); 206 | 207 | const CommentList = props => { 208 | const isSmall = useMediaQuery(theme => theme.breakpoints.down('sm')); 209 | 210 | return ( 211 | <List 212 | {...props} 213 | perPage={6} 214 | exporter={exporter} 215 | filters={<CommentFilter />} 216 | pagination={<CommentPagination />} 217 | component="div" 218 | > 219 | {isSmall ? <CommentMobileList /> : <CommentGrid />} 220 | </List> 221 | ); 222 | }; 223 | 224 | export default CommentList; 225 | -------------------------------------------------------------------------------- /src/comments/CommentShow.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | DateField, 4 | ReferenceField, 5 | Show, 6 | SimpleShowLayout, 7 | TextField, 8 | } from 'react-admin'; // eslint-disable-line import/no-unresolved 9 | 10 | const CommentShow = props => ( 11 | <Show {...props}> 12 | <SimpleShowLayout> 13 | <TextField source="id" /> 14 | <ReferenceField source="post_id" reference="posts"> 15 | <TextField source="title" /> 16 | </ReferenceField> 17 | <TextField source="author.name" /> 18 | <DateField source="created_at" /> 19 | <TextField source="body" /> 20 | </SimpleShowLayout> 21 | </Show> 22 | ); 23 | 24 | export default CommentShow; 25 | -------------------------------------------------------------------------------- /src/comments/PostPreview.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { SimpleShowLayout, TextField } from 'react-admin'; 4 | 5 | const PostPreview = props => { 6 | const record = useSelector( 7 | state => 8 | state.admin.resources[props.resource] 9 | ? state.admin.resources[props.resource].data[props.id] 10 | : null, 11 | [props.resource, props.id] 12 | ); 13 | const version = useSelector(state => state.admin.ui.viewVersion); 14 | useSelector(state => state.admin.loading > 0); 15 | 16 | return ( 17 | <SimpleShowLayout version={version} record={record} {...props}> 18 | <TextField source="id" /> 19 | <TextField source="title" /> 20 | <TextField source="teaser" /> 21 | </SimpleShowLayout> 22 | ); 23 | }; 24 | 25 | export default PostPreview; 26 | -------------------------------------------------------------------------------- /src/comments/PostQuickCreate.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useCallback } from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import { useSelector, useDispatch } from 'react-redux'; 5 | import { makeStyles } from '@material-ui/core/styles'; 6 | import { 7 | CREATE, 8 | SaveButton, 9 | SimpleForm, 10 | TextInput, 11 | Toolbar, 12 | required, 13 | showNotification, 14 | } from 'react-admin'; // eslint-disable-line import/no-unresolved 15 | 16 | import CancelButton from './PostQuickCreateCancelButton'; 17 | 18 | // We need a custom toolbar to add our custom buttons 19 | // The CancelButton allows to close the modal without submitting anything 20 | const PostQuickCreateToolbar = ({ submitting, onCancel, ...props }) => ( 21 | <Toolbar {...props} disableGutters> 22 | <SaveButton /> 23 | <CancelButton onClick={onCancel} /> 24 | </Toolbar> 25 | ); 26 | 27 | PostQuickCreateToolbar.propTypes = { 28 | submitting: PropTypes.bool, 29 | onCancel: PropTypes.func.isRequired, 30 | }; 31 | 32 | const useStyles = makeStyles({ 33 | form: { padding: 0 }, 34 | }); 35 | 36 | const PostQuickCreate = ({ onCancel, onSave, ...props }) => { 37 | const classes = useStyles(); 38 | const dispatch = useDispatch(); 39 | const submitting = useSelector(state => state.admin.loading > 0); 40 | 41 | const handleSave = useCallback( 42 | values => { 43 | dispatch({ 44 | type: 'QUICK_CREATE', 45 | payload: { data: values }, 46 | meta: { 47 | fetch: CREATE, 48 | resource: 'posts', 49 | onSuccess: { 50 | callback: ({ payload: { data } }) => onSave(data), 51 | }, 52 | onFailure: { 53 | callback: ({ error }) => { 54 | dispatch(showNotification(error.message, 'error')); 55 | }, 56 | }, 57 | }, 58 | }); 59 | }, 60 | [dispatch, onSave] 61 | ); 62 | 63 | return ( 64 | <SimpleForm 65 | save={handleSave} 66 | saving={submitting} 67 | redirect={false} 68 | toolbar={ 69 | <PostQuickCreateToolbar 70 | onCancel={onCancel} 71 | submitting={submitting} 72 | /> 73 | } 74 | classes={{ form: classes.form }} 75 | {...props} 76 | > 77 | <TextInput source="title" validate={required()} /> 78 | <TextInput 79 | source="teaser" 80 | validate={required()} 81 | fullWidth={true} 82 | multiline={true} 83 | /> 84 | </SimpleForm> 85 | ); 86 | }; 87 | 88 | PostQuickCreate.propTypes = { 89 | onCancel: PropTypes.func.isRequired, 90 | onSave: PropTypes.func.isRequired, 91 | }; 92 | 93 | export default PostQuickCreate; 94 | -------------------------------------------------------------------------------- /src/comments/PostQuickCreateCancelButton.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import Button from '@material-ui/core/Button'; 5 | import IconCancel from '@material-ui/icons/Cancel'; 6 | import { makeStyles } from '@material-ui/core/styles'; 7 | 8 | import { useTranslate } from 'react-admin'; 9 | 10 | const useStyles = makeStyles({ 11 | button: { 12 | margin: '10px 24px', 13 | position: 'relative', 14 | }, 15 | iconPaddingStyle: { 16 | paddingRight: '0.5em', 17 | }, 18 | }); 19 | 20 | const PostQuickCreateCancelButton = ({ 21 | onClick, 22 | label = 'ra.action.cancel', 23 | }) => { 24 | const translate = useTranslate(); 25 | const classes = useStyles(); 26 | return ( 27 | <Button className={classes.button} onClick={onClick}> 28 | <IconCancel className={classes.iconPaddingStyle} /> 29 | {label && translate(label, { _: label })} 30 | </Button> 31 | ); 32 | }; 33 | 34 | PostQuickCreateCancelButton.propTypes = { 35 | label: PropTypes.string, 36 | onClick: PropTypes.func.isRequired, 37 | }; 38 | 39 | export default PostQuickCreateCancelButton; 40 | -------------------------------------------------------------------------------- /src/comments/PostReferenceInput.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Fragment, useState, useCallback, useEffect } from 'react'; 3 | import { useDispatch } from 'react-redux'; 4 | import { FormSpy, useForm } from 'react-final-form'; 5 | 6 | import { makeStyles } from '@material-ui/core/styles'; 7 | import Button from '@material-ui/core/Button'; 8 | import Dialog from '@material-ui/core/Dialog'; 9 | import DialogTitle from '@material-ui/core/DialogTitle'; 10 | import DialogContent from '@material-ui/core/DialogContent'; 11 | import DialogActions from '@material-ui/core/DialogActions'; 12 | 13 | import { 14 | crudGetMatching, 15 | ReferenceInput, 16 | SelectInput, 17 | useTranslate, 18 | } from 'react-admin'; // eslint-disable-line import/no-unresolved 19 | 20 | import PostQuickCreate from './PostQuickCreate'; 21 | import PostPreview from './PostPreview'; 22 | 23 | const useStyles = makeStyles({ 24 | button: { 25 | margin: '10px 24px', 26 | position: 'relative', 27 | }, 28 | }); 29 | 30 | const PostReferenceInput = props => { 31 | const translate = useTranslate(); 32 | const classes = useStyles(); 33 | const dispatch = useDispatch(); 34 | const { change } = useForm(); 35 | 36 | const [showCreateDialog, setShowCreateDialog] = useState(false); 37 | const [showPreviewDialog, setShowPreviewDialog] = useState(false); 38 | const [newPostId, setNewPostId] = useState(''); 39 | 40 | useEffect(() => { 41 | //Refresh the choices of the ReferenceInput to ensure our newly created post 42 | // always appear, even after selecting another post 43 | dispatch( 44 | crudGetMatching( 45 | 'posts', 46 | 'comments@post_id', 47 | { page: 1, perPage: 25 }, 48 | { field: 'id', order: 'DESC' }, 49 | {} 50 | ) 51 | ); 52 | }, [dispatch, newPostId]); 53 | 54 | const handleNewClick = useCallback( 55 | event => { 56 | event.preventDefault(); 57 | setShowCreateDialog(true); 58 | }, 59 | [setShowCreateDialog] 60 | ); 61 | 62 | const handleShowClick = useCallback( 63 | event => { 64 | event.preventDefault(); 65 | setShowPreviewDialog(true); 66 | }, 67 | [setShowPreviewDialog] 68 | ); 69 | 70 | const handleCloseCreate = useCallback(() => { 71 | setShowCreateDialog(false); 72 | }, [setShowCreateDialog]); 73 | 74 | const handleCloseShow = useCallback(() => { 75 | setShowPreviewDialog(false); 76 | }, [setShowPreviewDialog]); 77 | 78 | const handleSave = useCallback( 79 | post => { 80 | setShowCreateDialog(false); 81 | setNewPostId(post.id); 82 | change('post_id', post.id); 83 | }, 84 | [setShowCreateDialog, setNewPostId, change] 85 | ); 86 | 87 | return ( 88 | <Fragment> 89 | <ReferenceInput {...props} defaultValue={newPostId}> 90 | <SelectInput optionText="title" /> 91 | </ReferenceInput> 92 | <Button 93 | data-testid="button-add-post" 94 | className={classes.button} 95 | onClick={handleNewClick} 96 | > 97 | {translate('ra.action.create')} 98 | </Button> 99 | <FormSpy 100 | subscription={{ values: true }} 101 | render={({ values }) => 102 | values.post_id ? ( 103 | <Fragment> 104 | <Button 105 | data-testid="button-show-post" 106 | className={classes.button} 107 | onClick={handleShowClick} 108 | > 109 | {translate('ra.action.show')} 110 | </Button> 111 | <Dialog 112 | data-testid="dialog-show-post" 113 | fullWidth 114 | open={showPreviewDialog} 115 | onClose={handleCloseShow} 116 | aria-label={translate('simple.create-post')} 117 | > 118 | <DialogTitle> 119 | {translate('simple.create-post')} 120 | </DialogTitle> 121 | <DialogContent> 122 | <PostPreview 123 | id={values.post_id} 124 | basePath="/posts" 125 | resource="posts" 126 | /> 127 | </DialogContent> 128 | <DialogActions> 129 | <Button 130 | data-testid="button-close-modal" 131 | onClick={handleCloseShow} 132 | > 133 | {translate('simple.action.close')} 134 | </Button> 135 | </DialogActions> 136 | </Dialog> 137 | </Fragment> 138 | ) : null 139 | } 140 | /> 141 | <Dialog 142 | data-testid="dialog-add-post" 143 | fullWidth 144 | open={showCreateDialog} 145 | onClose={handleCloseCreate} 146 | aria-label={translate('simple.create-post')} 147 | > 148 | <DialogTitle>{translate('simple.create-post')}</DialogTitle> 149 | <DialogContent> 150 | <PostQuickCreate 151 | onCancel={handleCloseCreate} 152 | onSave={handleSave} 153 | basePath="/posts" 154 | resource="posts" 155 | /> 156 | </DialogContent> 157 | </Dialog> 158 | </Fragment> 159 | ); 160 | }; 161 | 162 | export default PostReferenceInput; 163 | -------------------------------------------------------------------------------- /src/comments/index.js: -------------------------------------------------------------------------------- 1 | import ChatBubbleIcon from '@material-ui/icons/ChatBubble'; 2 | import CommentCreate from './CommentCreate'; 3 | import CommentEdit from './CommentEdit'; 4 | import CommentList from './CommentList'; 5 | import { ShowGuesser } from 'react-admin'; 6 | 7 | export default { 8 | list: CommentList, 9 | create: CommentCreate, 10 | edit: CommentEdit, 11 | show: ShowGuesser, 12 | icon: ChatBubbleIcon, 13 | }; 14 | -------------------------------------------------------------------------------- /src/data.js: -------------------------------------------------------------------------------- 1 | export default { 2 | posts: [ 3 | { 4 | id: 1, 5 | title: 6 | 'Accusantium qui nihil voluptatum quia voluptas maxime ab similique', 7 | teaser: 8 | 'In facilis aut aut odit hic doloribus. Fugit possimus perspiciatis sit molestias in. Sunt dignissimos sed quis at vitae veniam amet. Sint sunt perspiciatis quis doloribus aperiam numquam consequatur et. Blanditiis aut earum incidunt eos magnam et voluptatem. Minima iure voluptatum autem. At eaque sit aperiam minima aut in illum.', 9 | body: 10 | '<p>Rerum velit quos est <strong>similique</strong>. Consectetur tempora eos ullam velit nobis sit debitis. Magni explicabo omnis delectus labore vel recusandae.</p><p>Aut a minus laboriosam harum placeat quas minima fuga. Quos nulla fuga quam officia tempore. Rerum occaecati ut eum et tempore. Nam ab repudiandae et nemo praesentium.</p><p>Cumque corporis officia occaecati ducimus sequi laborum omnis ut. Nam aspernatur veniam fugit. Nihil eum libero ea dolorum ducimus impedit sed. Quidem inventore porro corporis debitis eum in. Nesciunt unde est est qui nulla. Esse sunt placeat molestiae molestiae sed quia. Sunt qui quidem quos velit reprehenderit quos blanditiis ducimus. Sint et molestiae maxime ut consequatur minima. Quaerat rem voluptates voluptatem quos. Corporis perferendis in provident iure. Commodi odit exercitationem excepturi et deserunt qui.</p><p>Optio iste necessitatibus velit non. Neque sed occaecati culpa porro culpa. Quia quam in molestias ratione et necessitatibus consequatur. Est est tempora consequatur voluptatem vel. Mollitia tenetur non quis omnis perspiciatis deserunt sed necessitatibus. Ad rerum reiciendis sunt aspernatur.</p><p>Est ullam ut magni aspernatur. Eum et sed tempore modi.</p><p>Earum aperiam sit neque quo laborum suscipit unde. Expedita nostrum itaque non non adipisci. Ut delectus quis delectus est at sint. Iste hic qui ea eaque eaque sed id. Hic placeat rerum numquam id velit deleniti voluptatem. Illum adipisci voluptas adipisci ut alias. Earum exercitationem iste quidem eveniet aliquid hic reiciendis. Exercitationem est sunt in minima consequuntur. Aut quaerat libero dolorem.</p>', 11 | views: 143, 12 | average_note: 2.72198, 13 | commentable: true, 14 | pictures: [ 15 | { 16 | name: 'the picture name', 17 | url: 'http://www.photo-libre.fr/paysage/1.jpg', 18 | metas: { 19 | title: 'This is a great photo', 20 | definitions: ['72', '300'], 21 | authors: [ 22 | { 23 | name: 'Paul', 24 | email: 'paul@email.com', 25 | }, 26 | { 27 | name: 'Joe', 28 | email: 'joe@email.com', 29 | }, 30 | ], 31 | }, 32 | }, 33 | { 34 | name: 'better name', 35 | url: 'http://www.photo-libre.fr/paysage/2.jpg', 36 | }, 37 | ], 38 | published_at: new Date('2012-08-06'), 39 | tags: [1, 3], 40 | category: 'tech', 41 | subcategory: 'computers', 42 | backlinks: [ 43 | { 44 | date: '2012-08-09T00:00:00.000Z', 45 | url: 'http://example.com/bar/baz.html', 46 | }, 47 | ], 48 | notifications: [12, 31, 42], 49 | }, 50 | { 51 | id: 2, 52 | title: 'Sint dignissimos in architecto aut', 53 | teaser: 54 | 'Quam earum itaque corrupti labore quas nihil sed. Dolores sunt culpa voluptates exercitationem eveniet totam rerum. Molestias perspiciatis rem numquam accusamus.', 55 | body: 56 | '<p>Aliquam magni <em>tempora</em> quas enim. Perspiciatis libero corporis sunt eum nam. Molestias est sunt molestiae natus.</p><p>Blanditiis dignissimos autem culpa itaque. Explicabo perferendis ullam officia ut quia nemo. Eaque perspiciatis perspiciatis est hic non ullam et. Expedita exercitationem enim sit ut dolore.</p><h2>Sed in sunt officia blanditiis ipsam maiores perspiciatis amet</h2><p>Vero fugiat facere officiis aut quis rerum velit. Autem eius sint ullam. Nemo sunt molestiae nulla accusantium est voluptatem voluptas sed. In blanditiis neque libero voluptatem praesentium occaecati nulla libero. Perspiciatis eos voluptatem facere voluptatibus. Explicabo quo eveniet nihil culpa. Qui eos officia consequuntur sed esse praesentium dolorum. Eius perferendis qui quia autem nostrum sed. Illum in ex excepturi voluptas. Qui veniam sit alias delectus nihil. Impedit est ut alias illum repellendus qui.</p><p>Veniam est aperiam quisquam soluta. Magni blanditiis praesentium sed similique velit ipsam consequatur. Porro omnis magni sunt incidunt aspernatur ut.</p>', 57 | views: 563, 58 | average_note: 3.48121, 59 | commentable: true, 60 | published_at: new Date('2012-08-08'), 61 | tags: [3, 5], 62 | category: 'lifestyle', 63 | backlinks: [], 64 | notifications: [], 65 | }, 66 | { 67 | id: 3, 68 | title: 'Perspiciatis adipisci vero qui ipsam iure porro', 69 | teaser: 70 | 'Ut ad consequatur esse illum. Ex dolore porro et ut sit. Commodi qui sed et voluptatibus laudantium.', 71 | body: 72 | '<p>Voluptatibus fugit sit praesentium voluptas vero vel. Reprehenderit quam cupiditate deleniti ipsum nisi qui. Molestiae modi sequi vel quibusdam est aliquid doloribus. Necessitatibus et excepturi alias necessitatibus magnam ea.</p><p>Dolor illum dolores qui et pariatur inventore incidunt molestias. Exercitationem ipsum voluptatibus voluptatum velit sint vel qui. Odit mollitia minus vitae impedit voluptatem. Voluptas ullam temporibus inventore fugiat pariatur odit molestias.</p><p>Atque est qui alias eum. Quibusdam rem ut dolores voluptate totam. Sit cumque perferendis sed a iusto laudantium quae et. Voluptatibus vitae natus quia laboriosam et deserunt. Doloribus fuga aut quo tempora animi eaque consequatur laboriosam.</p>', 73 | views: 467, 74 | commentable: true, 75 | published_at: new Date('2012-08-08'), 76 | tags: [1, 2], 77 | category: 'tech', 78 | backlinks: [ 79 | { 80 | date: '2012-08-10T00:00:00.000Z', 81 | url: 'http://example.com/foo/bar.html', 82 | }, 83 | { 84 | date: '2012-08-14T00:00:00.000Z', 85 | url: 'https://blog.johndoe.com/2012/08/12/foobar.html', 86 | }, 87 | { 88 | date: '2012-08-22T00:00:00.000Z', 89 | url: 'https://foo.bar.com/lorem/ipsum', 90 | }, 91 | { 92 | date: '2012-08-29T00:00:00.000Z', 93 | url: 'http://dicta.es/nam_doloremque', 94 | }, 95 | ], 96 | notifications: [12, 31, 42], 97 | }, 98 | { 99 | id: 4, 100 | title: 'Maiores et itaque aut perspiciatis', 101 | teaser: 102 | 'Et quo voluptas odit veniam omnis dolores. Odit commodi consequuntur necessitatibus dolorem officia. Reiciendis quas exercitationem libero sed. Itaque non facilis sit tempore aut doloribus.', 103 | body: 104 | '<p>Sunt sunt aut est et consequatur ea dolores. Voluptatem rerum cupiditate dolore. Voluptas sit sapiente corrupti error ducimus. Qui enim aut possimus qui. Impedit voluptatem sed inventore iusto et ut et. Maxime sunt qui adipisci expedita quisquam. Velit ea ut in blanditiis eos doloribus.</p><p>Qui optio ad magnam eius. Est id velit ratione eum corrupti non vitae. Quam consequatur animi sed corrupti quae sed deserunt. Accusamus eius eos recusandae eum quia id.</p><p>Voluptas omnis omnis culpa est vel eum. Ut in tempore harum voluptates odit delectus sit et. Consequuntur quod nihil veniam natus placeat provident. Totam ut fuga vitae in. Possimus cumque quae voluptatem asperiores vitae officiis dolores. Qui autem eos dolores eius. Iure ut delectus quis voluptatem. Velit at incidunt minus laboriosam culpa. Pariatur ipsa ut enim dolor. Sed magni sunt molestiae voluptas ut illum. Sit consequuntur laborum aliquid delectus in. Consectetur dicta asperiores itaque aut mollitia. Minus praesentium officiis voluptas a officiis ad beatae.</p>', 105 | views: 685, 106 | average_note: 1.2319, 107 | commentable: false, 108 | published_at: new Date('2012-08-12'), 109 | tags: [], 110 | category: 'lifestyle', 111 | notifications: [12, 31, 42], 112 | }, 113 | { 114 | id: 5, 115 | title: 'Sed quo et et fugiat modi', 116 | teaser: 117 | 'Consequuntur id aut soluta aspernatur sit. Aut doloremque recusandae sit saepe ut quas earum. Quae pariatur iure et ducimus non. Cupiditate dolorem itaque in sit.', 118 | body: 119 | '<p>Aut molestiae quae explicabo voluptas. Assumenda ea ipsam quia. Rerum rerum magnam sunt doloremque dolorem nulla. Eveniet ut aliquam est dignissimos nisi molestias dicta. Dolorum et id esse illum. Ea omnis nesciunt tempore et aut. Ut ullam totam doloribus recusandae est natus voluptatum officiis. Ea quam eos velit ipsam non accusamus praesentium.</p><p>Animi et minima alias sint. Reiciendis qui ipsam autem fugit consequuntur veniam. Vel cupiditate voluptas enim dolore cum ad. Ut iusto eius et.</p><p>Quis praesentium aut aut aut voluptas et. Quam laudantium at laudantium amet. Earum quidem eos earum quaerat nihil libero quia sed.</p><p>Autem voluptatem nostrum ullam numquam quis. Et aut unde nesciunt officiis nam eos ut distinctio. Animi est explicabo voluptas officia quos necessitatibus. Omnis debitis unde et qui rerum. Nisi repudiandae autem mollitia dolorum veritatis aut. Rem temporibus labore repellendus enim consequuntur dicta autem. Illum illo inventore possimus officiis quidem.</p><p>Ullam accusantium eaque perspiciatis. Quidem dolor minus aut quidem. Praesentium earum beatae eos eligendi nostrum. Dolor nam quo aut.</p><p>Accusamus aut tempora omnis magni sit quos eos aut. Vitae ut inventore facere neque rerum. Qui esse rem cupiditate sit.</p><p>Est minus odio sint reprehenderit. Consectetur dolores eligendi et quaerat sint vel magni. Voluptatum hic cum placeat ad ea reiciendis laborum et. Eos ab id suscipit.</p>', 120 | views: 559, 121 | average_note: 3, 122 | commentable: true, 123 | published_at: new Date('2012-08-05'), 124 | category: 'tech', 125 | notifications: [12, 31, 42], 126 | }, 127 | { 128 | id: 6, 129 | title: 'Minima ea vero omnis odit officiis aut', 130 | teaser: 131 | 'Omnis rerum voluptatem illum. Amet totam minus id qui aspernatur. Adipisci commodi velit sapiente architecto et molestias. Maiores doloribus quis occaecati quidem laborum. Quae quia quaerat est itaque. Vero assumenda quia tempora libero dicta quis asperiores magnam. Necessitatibus accusantium saepe commodi ut.', 132 | body: 133 | '<p>Sit autem rerum inventore repellendus. Enim placeat est ea dolor voluptas nisi alias. Repellat quam laboriosam repudiandae illum similique omnis non exercitationem. Modi mollitia omnis sed vel et expedita fugiat. Esse laboriosam doloribus deleniti atque quidem praesentium aliquid. Error animi ab excepturi quia. Et voluptates voluptatem et est quibusdam aspernatur. Fugiat consequatur veritatis commodi enim quaerat sint. Quis quae fuga exercitationem dolorem enim laborum numquam. Iste necessitatibus repellat in ea nihil et rem. Corporis dolores sed vitae consectetur dolores qui dicta. Laudantium et suscipit odit quidem qui. Provident libero eveniet distinctio debitis odio cum id dolorum. Consequuntur laboriosam qui ut magni sit dicta. Distinctio fugit voluptatibus voluptatem suscipit incidunt ut cupiditate. Magni harum in aut alias veniam. Eos aut impedit ut et. Iure aliquid adipisci aliquam et ab et qui. Itaque quod consequuntur dolore asperiores architecto neque. Exercitationem eum voluptas ut quis hic quo. Omnis quas porro laudantium. Qui magnam et totam quibusdam in quo. Impedit laboriosam eum sint soluta facere ut voluptatem.</p>', 134 | views: 208, 135 | average_note: 3.1214, 136 | published_at: new Date('2012-09-05'), 137 | tags: [1, 4], 138 | category: 'tech', 139 | notifications: [42], 140 | }, 141 | { 142 | id: 7, 143 | title: 'Illum veritatis corrupti exercitationem sed velit', 144 | teaser: 145 | 'Omnis hic quo aperiam fugiat iure amet est. Molestias ratione aut et dolor earum magnam placeat. Ad a quam ea amet hic omnis rerum.', 146 | body: 147 | '<p>Omnis sunt maxime qui consequatur perspiciatis et dolor. Assumenda numquam sit rerum aut dolores. Repudiandae rerum et quisquam. Perferendis cupiditate sequi non similique eum accusamus voluptas.</p><p>Officiis in voluptatum culpa ut eaque laborum. Sit quos velit sed ad voluptates. Alias aut quo accusantium aut cumque perferendis. Numquam rerum vel et est delectus. Mollitia dolores voluptatum accusantium id rem. Autem dolorem similique earum. Deleniti qui iusto et vero. Enim quaerat ipsum omnis magni. Autem magnam vero nulla impedit distinctio. Sequi laudantium ut animi enim recusandae et voluptatum. Dicta architecto nostrum voluptas consequuntur ea. Porro odio illo praesentium qui. Quia sit sed labore porro. Minima odit nemo sint praesentium. Ea sapiente quis aut. Qui cumque aut repudiandae in. Ipsam mollitia ab vitae iusto maxime. Eaque qui impedit et ea dolor aut. Tenetur ut nihil sed. Eum doloremque harum ipsam vel eos ut enim.</p>', 148 | views: 133, 149 | average_note: null, 150 | commentable: true, 151 | published_at: new Date('2012-09-29'), 152 | tags: [3, 4], 153 | category: 'tech', 154 | notifications: [12, 31], 155 | }, 156 | { 157 | id: 8, 158 | title: 159 | 'Culpa possimus quibusdam nostrum enim tempore rerum odit excepturi', 160 | teaser: 161 | 'Qui quos exercitationem itaque quia. Repellat libero ut recusandae quidem repudiandae ipsam laudantium. Eveniet quos et quo omnis aut commodi incidunt.', 162 | body: 163 | '<p>Laudantium voluptatem non facere officiis qui natus natus. Ex perspiciatis quia dolor earum. In rerum deleniti voluptas quo quia adipisci voluptatibus.</p><p>Mollitia eos quaerat ad. Et non aliquam velit. Doloremque repudiandae earum suscipit deleniti.</p><p>Debitis voluptatem possimus saepe. Rerum nam est neque voluptate quae ratione et quaerat. Fugiat et ullam adipisci numquam. Atque qui cum quae quod qui reprehenderit. Veritatis odio eligendi est odit minima ut dolores. Blanditiis aut rem aliquam nulla esse odit. Quibusdam quam natus eos tenetur nemo eligendi velit nam. Consequatur libero eius quia impedit neque fuga. Accusantium sunt accusantium eaque illum dicta. Expedita explicabo quia soluta.</p><p>Dolores aperiam rem velit id provident quo ea. Modi illum voluptate corrupti recusandae optio. Voluptatem architecto numquam reiciendis quo nostrum suscipit. Dolore repellat deleniti nihil omnis illum explicabo nihil. Alias maxime hic minus voluptas odio id dolorum. Neque perferendis repellendus autem consequatur consequatur doloribus. Sit aspernatur nisi aliquam rem voluptas occaecati.</p><p>In eveniet nostrum culpa totam officia doloremque. Fugiat maxime magni aut magnam praesentium vel facere. Tempora soluta possimus omnis modi et qui minus. Consequatur et suscipit autem quia nulla.</p><p>Qui eum aliquid inventore at. Qui provident perspiciatis sed eum eos sunt eveniet autem. Ducimus velit tenetur sed. Quas laboriosam dicta ipsa id fugiat. Hic nihil laboriosam atque natus. Quam natus esse est error molestiae nulla. Odit ut dolorem laborum quidem quis alias. Labore sint porro et reprehenderit ut dolorem vel dolorum. Dolores suscipit ut dolores possimus id dicta cupiditate. Est cum dolorum dolores ducimus quia reprehenderit. Iste suscipit molestias voluptatem molestiae. Nostrum modi dicta qui deleniti. Reprehenderit voluptatem soluta non in labore. Voluptatem ut illo illo harum voluptas cumque. Tempora illo distinctio qui aut.</p><p>Eaque voluptatem eos omnis qui dolor non possimus. Distinctio ratione facere doloremque rerum qui voluptas et. Cum incidunt numquam molestias et labore odio sunt aut. Aut pariatur dignissimos est atque.</p>', 164 | views: 557, 165 | average_note: null, 166 | commentable: false, 167 | published_at: new Date('2012-10-02'), 168 | tags: [5, 1], 169 | category: 'lifestyle', 170 | notifications: [12, 31, 42], 171 | }, 172 | { 173 | id: 9, 174 | title: 'A voluptas eius eveniet ut commodi dolor', 175 | teaser: 176 | 'Sed necessitatibus nesciunt nesciunt aut non sunt. Quam ut in a sed ducimus eos qui sint. Commodi illo necessitatibus sint explicabo maiores. Maxime voluptates sit distinctio quo excepturi. Qui aliquid debitis repellendus distinctio et aut. Ex debitis et quasi id.', 177 | body: 178 | '<p>Consequatur temporibus explicabo vel laudantium totam. Voluptates nihil numquam accusamus ut unde quo. Molestiae dolores quas sit aliquam. Sit et fuga necessitatibus natus fugit voluptas et. Esse vitae sed sit eius.</p><p>Accusantium aliquam accusamus illo eum. Excepturi molestiae et earum qui. Iste dolor eligendi est vero iure eos nesciunt. Qui aspernatur repellendus id rerum consequatur ut. Quis ab quos fugit dicta aut voluptas. Rerum aut esse dolor. Illo iste ullam possimus nam nam assumenda molestiae est.</p><p>In porro nesciunt cumque in sint vel architecto. Aliquam et in numquam quae explicabo. Deserunt suscipit sunt excepturi optio molestiae. Facilis saepe eaque commodi provident ad voluptates eligendi.</p><p>Magnam et neque ad sed qui laborum et. Aut dolorem maxime harum. Molestias aut facere vitae voluptatem.</p><p>Excepturi odit doloremque eos quisquam sunt. Veniam repudiandae nisi dolorum nam quos. Qui voluptatem enim enim. Dolorum eveniet eaque expedita est tempore. Expedita amet blanditiis esse qui. Nam dolor odio nihil nobis quas quia exercitationem. Iusto ut ut reiciendis sint laudantium et distinctio. Vitae architecto accusamus quos dolores laudantium doloribus alias. Est est esse autem repellat. Assumenda officia aperiam sequi facere distinctio ut. Magnam qui assumenda eligendi sint. Architecto autem harum qui ea quos ut nesciunt et. Optio quidem sit ex quos provident. Et dolor dicta et laudantium. Incidunt id quo enim atque molestiae quam repudiandae omnis. Sed nam voluptatem dolores natus quisquam. Sit nostrum voluptate sed asperiores. Saepe eaque et illum aperiam. Maxime tenetur sunt reiciendis.</p><p>Ducimus quia dolorem voluptas ea. Fuga eum architecto eius cum est quibusdam eligendi est. In ut aperiam ea ut.</p>', 179 | views: 143, 180 | average_note: 3.1214, 181 | commentable: true, 182 | published_at: new Date('2012-10-16'), 183 | tags: [], 184 | category: 'tech', 185 | notifications: [12, 31, 42], 186 | }, 187 | { 188 | id: 10, 189 | title: 'Totam vel quasi a odio et nihil', 190 | teaser: 191 | 'Excepturi veritatis velit rerum nemo voluptatem illum tempora eos. Et impedit sed qui et iusto. A alias asperiores quia quo.', 192 | body: 193 | '<p>Voluptas iure consequatur repudiandae quibusdam iure. Quibusdam consequatur sit cupiditate aut eum iure. Provident ut aut est itaque ut eligendi sunt.</p><p>Odio ipsa dolore rem occaecati voluptatum neque. Quia est minima totam est dicta aliquid sed. Doloribus ea eligendi qui odit. Consectetur aut illum aspernatur exercitationem ut. Distinctio sapiente doloribus beatae natus mollitia. Nostrum cum magni autem expedita natus est nulla totam.</p><p>Et possimus quia aliquam est molestiae eum. Dicta nostrum ea rerum omnis. Ut hic amet sequi commodi voluptatem ut. Nulla magni totam placeat asperiores error.</p>', 194 | views: 721, 195 | average_note: 4.121, 196 | commentable: true, 197 | published_at: new Date('2012-10-19'), 198 | tags: [1, 4], 199 | category: 'lifestyle', 200 | notifications: [12, 31, 42], 201 | }, 202 | { 203 | id: 11, 204 | title: 'Omnis voluptate enim similique est possimus', 205 | teaser: 206 | 'Velit eos vero reprehenderit ut assumenda saepe qui. Quasi aut laboriosam quas voluptate voluptatem. Et eos officia repudiandae quaerat. Mollitia libero numquam laborum eos.', 207 | body: 208 | '<p>Ut qui a quis culpa impedit. Harum quae sunt aspernatur dolorem minima et dolorum. Consequatur sunt eveniet sit perspiciatis fuga praesentium. Quam voluptatem a ullam accusantium debitis eum consectetur.</p><p>Voluptas rem impedit omnis maiores saepe. Eum consequatur ut et consequatur repellat. Quos dolorem dolorum nihil dolor sit optio velit. Quasi quaerat enim omnis ipsum.</p><p>Officia asperiores ut doloribus. Architecto iste quia illo non. Deleniti enim odio aut amet eveniet. Modi sint aut excepturi quisquam error sed officia. Nostrum enim repellendus inventore minus. Itaque vitae ipsam quasi. Qui provident vero ab facere. Sit enim provident doloremque minus quam. Voluptatem expedita est maiores nihil est voluptatem error. Asperiores ut a est ducimus hic optio. Natus omnis ullam consectetur ducimus nisi sint ducimus odit. Soluta cupiditate ipsam magnam.</p><p>Illum magni aut autem in sed iure. Ea explicabo ducimus officia corrupti ipsam minima minima. Nihil ab similique modi sunt unde nisi. Iusto quis iste ut aut earum magni. Nisi nisi minima sapiente quos aut libero maxime. Ut consequuntur sit vel odio suscipit fugiat tempore et. Et eveniet aut voluptatibus aliquid accusantium quis qui et. Veniam rem ut et. Vel officiis et voluptatum eaque ipsum sit. Sed iste rem ipsam dolor maiores. Et animi aspernatur aut error. Quisquam veritatis voluptatem magnam id. Blanditiis dolorem quo et voluptatum.</p>', 209 | views: 294, 210 | average_note: 3.12942, 211 | commentable: true, 212 | published_at: new Date('2012-10-22'), 213 | tags: [4, 3], 214 | category: 'tech', 215 | subcategory: 'computers', 216 | pictures: null, 217 | backlinks: [ 218 | { 219 | date: '2012-10-29T00:00:00.000Z', 220 | url: 'http://dicta.es/similique_pariatur', 221 | }, 222 | ], 223 | notifications: [12, 31, 42], 224 | }, 225 | { 226 | id: 12, 227 | title: 'Qui tempore rerum et voluptates', 228 | teaser: 229 | 'Occaecati rem perferendis dolor aut numquam cupiditate. At tenetur dolores pariatur et libero asperiores porro voluptas. Officiis corporis sed eos repellendus perferendis distinctio hic consequatur.', 230 | body: 231 | '<p>Praesentium corrupti minus molestias eveniet mollitia. Sit dolores est tenetur eos veritatis. Vero aut molestias provident ducimus odit optio.</p><p>Minima amet accusantium dolores et. Iste eos necessitatibus iure provident rerum repellendus reiciendis eos. Voluptate dolorem dolore aliquid sed maiores.</p><p>Ut quia excepturi quidem quidem. Cupiditate qui est rerum praesentium consequatur ad. Minima rem et est. Ut odio nostrum fugit laborum. Quis vitae occaecati tenetur earum non architecto.</p><p>Minima est nobis accusamus sunt explicabo fuga. Ut ut ut officia labore ratione animi saepe et.</p><p>Accusamus quae ex rerum est eos nesciunt et. Nemo nam consequatur earum necessitatibus et. Eum corporis corporis quia at nihil consectetur accusamus. Ea eveniet et culpa maxime.</p><p>Et et quisquam odio sapiente. Voluptas ducimus beatae ratione et soluta esse ut animi. Ipsa architecto veritatis cumque in.</p><p>Voluptatem dolore sint aliquam excepturi. Pariatur quisquam a eum. Aut et sit quis et dolorem omnis. Molestias id cupiditate error ab.</p><p>Odio ut deleniti incidunt vel dolores eligendi. Nemo aut commodi accusamus alias reprehenderit dolorum eaque. Iure fugit quis occaecati aspernatur tempora iste.</p><p>Omnis repellat et sequi numquam accusantium doloribus eum totam. Ab assumenda facere qui voluptate. Temporibus non ipsa officia. Corrupti omnis ut dolores velit aliquam ut omnis consequuntur.</p>', 232 | views: 719, 233 | average_note: 2, 234 | commentable: true, 235 | published_at: new Date('2012-11-07'), 236 | tags: [], 237 | category: 'lifestyle', 238 | subcategory: 'fitness', 239 | pictures: [], 240 | backlinks: [ 241 | { 242 | date: '2012-08-07T00:00:00.000Z', 243 | url: 'http://example.com/foo/bar.html', 244 | }, 245 | { 246 | date: '2012-08-12T00:00:00.000Z', 247 | url: 'https://blog.johndoe.com/2012/08/12/foobar.html', 248 | }, 249 | ], 250 | notifications: [12, 31, 42], 251 | }, 252 | { 253 | id: 13, 254 | title: 'Fusce massa lorem, pulvinar a posuere ut, accumsan ac nisi', 255 | teaser: 256 | 'Quam earum itaque corrupti labore quas nihil sed. Dolores sunt culpa voluptates exercitationem eveniet totam rerum. Molestias perspiciatis rem numquam accusamus.', 257 | body: 258 | '<p>Curabitur eu odio ullamcorper, pretium sem at, blandit libero. Nulla sodales facilisis libero, eu gravida tellus ultrices nec. In ut gravida mi. Vivamus finibus tortor tempus egestas lacinia. Cras eu arcu nisl. Donec pretium dolor ipsum, eget feugiat urna iaculis ut.</p> <p>Nullam lacinia accumsan diam, ac faucibus velit maximus ac. Donec eros ligula, ullamcorper sit amet varius eget, molestie nec sapien. Donec ac est non tellus convallis condimentum. Aliquam non vehicula mauris, ac rhoncus mi. Integer consequat ipsum a posuere ornare. Quisque mollis finibus libero scelerisque dapibus. </p>', 259 | views: 222, 260 | average_note: 4, 261 | commentable: true, 262 | published_at: new Date('2012-12-01'), 263 | tags: [3, 5], 264 | category: 'lifestyle', 265 | backlinks: [], 266 | notifications: [], 267 | }, 268 | ], 269 | comments: [ 270 | { 271 | id: 1, 272 | author: {}, 273 | post_id: 6, 274 | body: 275 | "Queen, tossing her head through the wood. 'If it had lost something; and she felt sure it.", 276 | created_at: new Date('2012-08-02'), 277 | }, 278 | { 279 | id: 2, 280 | author: { 281 | name: 'Kiley Pouros', 282 | email: 'kiley@gmail.com', 283 | }, 284 | post_id: 9, 285 | body: 286 | "White Rabbit: it was indeed: she was out of the ground--and I should frighten them out of its right paw round, 'lives a March Hare. 'Sixteenth,'.", 287 | created_at: new Date('2012-08-08'), 288 | }, 289 | { 290 | id: 3, 291 | author: { 292 | name: 'Justina Hegmann', 293 | }, 294 | post_id: 3, 295 | body: 296 | "I'm not Ada,' she said, 'and see whether it's marked \"poison\" or.", 297 | created_at: new Date('2012-08-02'), 298 | }, 299 | { 300 | id: 4, 301 | author: { 302 | name: 'Ms. Brionna Smitham MD', 303 | }, 304 | post_id: 6, 305 | body: 306 | "Dormouse. 'Fourteenth of March, I think I can say.' This was such a noise inside, no one else seemed inclined.", 307 | created_at: new Date('2014-09-24'), 308 | }, 309 | { 310 | id: 5, 311 | author: { 312 | name: 'Edmond Schulist', 313 | }, 314 | post_id: 1, 315 | body: 316 | "I ought to tell me your history, you know,' the Hatter and the happy summer days. THE.", 317 | created_at: new Date('2012-08-07'), 318 | }, 319 | { 320 | id: 6, 321 | author: { 322 | name: 'Danny Greenholt', 323 | }, 324 | post_id: 6, 325 | body: 326 | 'Duchess asked, with another hedgehog, which seemed to be lost: away went Alice after it, never once considering how in the other. In the very tones of.', 327 | created_at: new Date('2012-08-09'), 328 | }, 329 | { 330 | id: 7, 331 | author: { 332 | name: 'Luciano Berge', 333 | }, 334 | post_id: 5, 335 | body: 336 | "While the Panther were sharing a pie--' [later editions continued as follows.", 337 | created_at: new Date('2012-09-06'), 338 | }, 339 | { 340 | id: 8, 341 | author: { 342 | name: 'Annamarie Mayer', 343 | }, 344 | post_id: 5, 345 | body: 346 | "I tell you, you coward!' and at once and put it more clearly,' Alice.", 347 | created_at: new Date('2012-10-03'), 348 | }, 349 | { 350 | id: 9, 351 | author: { 352 | name: 'Breanna Gibson', 353 | }, 354 | post_id: 2, 355 | body: 356 | "THAT. Then again--\"BEFORE SHE HAD THIS FIT--\" you never tasted an egg!' 'I HAVE tasted eggs, certainly,' said Alice, as she spoke. Alice did not like to have it.", 357 | created_at: new Date('2012-11-06'), 358 | }, 359 | { 360 | id: 10, 361 | author: { 362 | name: 'Logan Schowalter', 363 | }, 364 | post_id: 3, 365 | body: 366 | "I'd been the whiting,' said the Hatter, it woke up again with a T!' said the Gryphon. '--you advance twice--' 'Each with a growl, And concluded the banquet--] 'What IS the fun?' said.", 367 | created_at: new Date('2012-12-07'), 368 | }, 369 | { 370 | id: 11, 371 | author: { 372 | name: 'Logan Schowalter', 373 | }, 374 | post_id: 1, 375 | body: 376 | "I don't want to be?' it asked. 'Oh, I'm not Ada,' she said, 'and see whether it's marked \"poison\" or not'; for she had asked it aloud; and in despair she put her hand on the end of the.", 377 | created_at: new Date('2012-08-05'), 378 | }, 379 | ], 380 | tags: [ 381 | { 382 | id: 1, 383 | name: 'Sport', 384 | published: 1, 385 | }, 386 | { 387 | id: 2, 388 | name: 'Technology', 389 | published: false, 390 | }, 391 | { 392 | id: 3, 393 | name: 'Code', 394 | published: true, 395 | }, 396 | { 397 | id: 4, 398 | name: 'Photo', 399 | published: false, 400 | }, 401 | { 402 | id: 5, 403 | name: 'Music', 404 | published: 1, 405 | }, 406 | { 407 | id: 6, 408 | name: 'Parkour', 409 | published: 1, 410 | parent_id: 1, 411 | }, 412 | { 413 | id: 7, 414 | name: 'Crossfit', 415 | published: 1, 416 | parent_id: 1, 417 | }, 418 | { 419 | id: 8, 420 | name: 'Computing', 421 | published: 1, 422 | parent_id: 2, 423 | }, 424 | { 425 | id: 9, 426 | name: 'Nanoscience', 427 | published: 1, 428 | parent_id: 2, 429 | }, 430 | { 431 | id: 10, 432 | name: 'Blockchain', 433 | published: 1, 434 | parent_id: 2, 435 | }, 436 | { 437 | id: 11, 438 | name: 'Node', 439 | published: 1, 440 | parent_id: 3, 441 | }, 442 | { 443 | id: 12, 444 | name: 'React', 445 | published: 1, 446 | parent_id: 3, 447 | }, 448 | { 449 | id: 13, 450 | name: 'Nature', 451 | published: 1, 452 | parent_id: 4, 453 | }, 454 | { 455 | id: 14, 456 | name: 'People', 457 | published: 1, 458 | parent_id: 4, 459 | }, 460 | { 461 | id: 15, 462 | name: 'Animals', 463 | published: 1, 464 | parent_id: 13, 465 | }, 466 | { 467 | id: 16, 468 | name: 'Moutains', 469 | published: 1, 470 | parent_id: 13, 471 | }, 472 | { 473 | id: 17, 474 | name: 'Rap', 475 | published: 1, 476 | parent_id: 5, 477 | }, 478 | { 479 | id: 18, 480 | name: 'Rock', 481 | published: 1, 482 | parent_id: 5, 483 | }, 484 | { 485 | id: 19, 486 | name: 'World', 487 | published: 1, 488 | parent_id: 5, 489 | }, 490 | ], 491 | users: [ 492 | { 493 | id: 1, 494 | name: 'Logan Schowalter', 495 | role: 'admin', 496 | }, 497 | { 498 | id: 2, 499 | name: 'Breanna Gibson', 500 | role: 'user', 501 | }, 502 | { 503 | id: 3, 504 | name: 'Annamarie Mayer', 505 | role: 'user', 506 | }, 507 | ], 508 | }; 509 | -------------------------------------------------------------------------------- /src/dataProvider.js: -------------------------------------------------------------------------------- 1 | import fakeRestProvider from 'ra-data-fakerest'; 2 | import { cacheDataProviderProxy } from 'react-admin'; 3 | 4 | import data from './data'; 5 | import addUploadFeature from './addUploadFeature'; 6 | 7 | const dataProvider = fakeRestProvider(data, true); 8 | const uploadCapableDataProvider = addUploadFeature(dataProvider); 9 | const sometimesFailsDataProvider = new Proxy(uploadCapableDataProvider, { 10 | get: (target, name) => (resource, params) => { 11 | // add rejection by type or resource here for tests, e.g. 12 | // if (name === 'delete' && resource === 'posts') { 13 | // return Promise.reject(new Error('deletion error')); 14 | // } 15 | if ( 16 | resource === 'posts' && 17 | params.data && 18 | params.data.title === 'f00bar' 19 | ) { 20 | return Promise.reject(new Error('this title cannot be used')); 21 | } 22 | return uploadCapableDataProvider[name](resource, params); 23 | }, 24 | }); 25 | const delayedDataProvider = new Proxy(sometimesFailsDataProvider, { 26 | get: (target, name) => (resource, params) => 27 | new Promise(resolve => 28 | setTimeout( 29 | () => 30 | resolve(sometimesFailsDataProvider[name](resource, params)), 31 | 300 32 | ) 33 | ), 34 | }); 35 | 36 | export default cacheDataProviderProxy(delayedDataProvider); 37 | -------------------------------------------------------------------------------- /src/i18n/en.js: -------------------------------------------------------------------------------- 1 | import englishMessages from 'ra-language-english'; 2 | 3 | export const messages = { 4 | simple: { 5 | action: { 6 | close: 'Close', 7 | resetViews: 'Reset views', 8 | }, 9 | 'create-post': 'New post', 10 | }, 11 | ...englishMessages, 12 | resources: { 13 | posts: { 14 | name: 'Post |||| Posts', 15 | fields: { 16 | average_note: 'Average note', 17 | body: 'Body', 18 | comments: 'Comments', 19 | commentable: 'Commentable', 20 | commentable_short: 'Com.', 21 | created_at: 'Created at', 22 | notifications: 'Notifications recipients', 23 | nb_view: 'Nb views', 24 | password: 'Password (if protected post)', 25 | pictures: 'Related Pictures', 26 | published_at: 'Published at', 27 | teaser: 'Teaser', 28 | tags: 'Tags', 29 | title: 'Title', 30 | views: 'Views', 31 | authors: 'Authors', 32 | }, 33 | }, 34 | comments: { 35 | name: 'Comment |||| Comments', 36 | fields: { 37 | body: 'Body', 38 | created_at: 'Created at', 39 | post_id: 'Posts', 40 | author: { 41 | name: 'Author', 42 | }, 43 | }, 44 | }, 45 | users: { 46 | name: 'User |||| Users', 47 | fields: { 48 | name: 'Name', 49 | role: 'Role', 50 | }, 51 | }, 52 | }, 53 | post: { 54 | list: { 55 | search: 'Search', 56 | }, 57 | form: { 58 | summary: 'Summary', 59 | body: 'Body', 60 | miscellaneous: 'Miscellaneous', 61 | comments: 'Comments', 62 | }, 63 | edit: { 64 | title: 'Post "%{title}"', 65 | }, 66 | action: { 67 | save_and_edit: 'Save and Edit', 68 | save_and_add: 'Save and Add', 69 | save_and_show: 'Save and Show', 70 | save_with_average_note: 'Save with Note', 71 | }, 72 | }, 73 | comment: { 74 | list: { 75 | about: 'About', 76 | }, 77 | }, 78 | user: { 79 | list: { 80 | search: 'Search', 81 | }, 82 | form: { 83 | summary: 'Summary', 84 | security: 'Security', 85 | }, 86 | edit: { 87 | title: 'User "%{title}"', 88 | }, 89 | action: { 90 | save_and_add: 'Save and Add', 91 | save_and_show: 'Save and Show', 92 | }, 93 | }, 94 | }; 95 | 96 | export default messages; 97 | -------------------------------------------------------------------------------- /src/i18n/fr.js: -------------------------------------------------------------------------------- 1 | import frenchMessages from 'ra-language-french'; 2 | 3 | export default { 4 | simple: { 5 | action: { 6 | close: 'Fermer', 7 | resetViews: 'Réinitialiser des vues', 8 | }, 9 | 'create-post': 'Nouveau post', 10 | }, 11 | ...frenchMessages, 12 | resources: { 13 | posts: { 14 | name: 'Article |||| Articles', 15 | fields: { 16 | average_note: 'Note moyenne', 17 | body: 'Contenu', 18 | comments: 'Commentaires', 19 | commentable: 'Commentable', 20 | commentable_short: 'Com.', 21 | created_at: 'Créé le', 22 | notifications: 'Destinataires de notifications', 23 | nb_view: 'Nb de vues', 24 | password: 'Mot de passe (si protégé)', 25 | pictures: 'Photos associées', 26 | published_at: 'Publié le', 27 | teaser: 'Description', 28 | tags: 'Catégories', 29 | title: 'Titre', 30 | views: 'Vues', 31 | authors: 'Auteurs', 32 | }, 33 | }, 34 | comments: { 35 | name: 'Commentaire |||| Commentaires', 36 | fields: { 37 | body: 'Contenu', 38 | created_at: 'Créé le', 39 | post_id: 'Article', 40 | author: { 41 | name: 'Auteur', 42 | }, 43 | }, 44 | }, 45 | users: { 46 | name: 'User |||| Users', 47 | fields: { 48 | name: 'Name', 49 | role: 'Role', 50 | }, 51 | }, 52 | }, 53 | post: { 54 | list: { 55 | search: 'Recherche', 56 | }, 57 | form: { 58 | summary: 'Résumé', 59 | body: 'Contenu', 60 | miscellaneous: 'Extra', 61 | comments: 'Commentaires', 62 | }, 63 | edit: { 64 | title: 'Article "%{title}"', 65 | }, 66 | }, 67 | comment: { 68 | list: { 69 | about: 'Au sujet de', 70 | }, 71 | }, 72 | user: { 73 | list: { 74 | search: 'Recherche', 75 | }, 76 | form: { 77 | summary: 'Résumé', 78 | security: 'Sécurité', 79 | }, 80 | edit: { 81 | title: 'Utilisateur "%{title}"', 82 | }, 83 | }, 84 | }; 85 | -------------------------------------------------------------------------------- /src/i18n/index.js: -------------------------------------------------------------------------------- 1 | import enMessages from './en'; 2 | import frMessages from './fr'; 3 | 4 | export const en = enMessages; 5 | export const fr = frMessages; 6 | -------------------------------------------------------------------------------- /src/i18nProvider.js: -------------------------------------------------------------------------------- 1 | import polyglotI18nProvider from 'ra-i18n-polyglot'; 2 | import englishMessages from './i18n/en'; 3 | 4 | const messages = { 5 | fr: () => import('./i18n/fr.js').then(messages => messages.default), 6 | }; 7 | 8 | export default polyglotI18nProvider(locale => { 9 | if (locale === 'fr') { 10 | return messages[locale](); 11 | } 12 | 13 | // Always fallback on english 14 | return englishMessages; 15 | }, 'en'); 16 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html> 3 | 4 | <head> 5 | <title>React Admin 6 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | 18 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint react/jsx-key: off */ 2 | import * as React from "react"; 3 | import { Admin, Resource } from "react-admin"; // eslint-disable-line import/no-unresolved 4 | import { render } from "react-dom"; 5 | import { Route } from "react-router-dom"; 6 | 7 | import authProvider from "./authProvider"; 8 | import comments from "./comments"; 9 | import CustomRouteLayout from "./CustomRouteLayout"; 10 | import ThemedCustomRouteNoLayout from "./ThemedCustomRouteNoLayout"; 11 | import dataProvider from "./dataProvider"; 12 | import i18nProvider from "./i18nProvider"; 13 | import Layout from "./Layout"; 14 | import posts from "./posts"; 15 | import users from "./users"; 16 | import tags from "./tags"; 17 | import { theme } from "./theme"; 18 | 19 | render( 20 | } 32 | noLayout 33 | />, 34 | } 38 | />, 39 | ]} 40 | > 41 | {(permissions) => [ 42 | , 43 | , 44 | permissions ? : null, 45 | , 46 | ]} 47 | , 48 | document.getElementById("root") 49 | ); 50 | -------------------------------------------------------------------------------- /src/posts/PostCreate.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useMemo } from 'react'; 3 | import RichTextInput from 'ra-input-rich-text'; 4 | import { 5 | ArrayInput, 6 | AutocompleteInput, 7 | BooleanInput, 8 | Create, 9 | DateInput, 10 | FormDataConsumer, 11 | NumberInput, 12 | ReferenceInput, 13 | SaveButton, 14 | SelectInput, 15 | SimpleForm, 16 | SimpleFormIterator, 17 | TextInput, 18 | Toolbar, 19 | required, 20 | } from 'react-admin'; // eslint-disable-line import/no-unresolved 21 | import { FormSpy } from 'react-final-form'; 22 | 23 | const PostCreateToolbar = props => ( 24 | 25 | 30 | 36 | 42 | ({ ...data, average_note: 10 })} 45 | redirect="show" 46 | submitOnEnter={false} 47 | variant="text" 48 | /> 49 | 50 | ); 51 | 52 | const backlinksDefaultValue = [ 53 | { 54 | date: new Date(), 55 | url: 'http://google.com', 56 | }, 57 | ]; 58 | const PostCreate = ({ permissions, ...props }) => { 59 | const initialValues = useMemo( 60 | () => ({ 61 | average_note: 0, 62 | }), 63 | [] 64 | ); 65 | 66 | const dateDefaultValue = useMemo(() => new Date(), []); 67 | 68 | return ( 69 | 70 | } 72 | initialValues={initialValues} 73 | validate={values => { 74 | const errors = {}; 75 | ['title', 'teaser'].forEach(field => { 76 | if (!values[field]) { 77 | errors[field] = 'Required field'; 78 | } 79 | }); 80 | 81 | if (values.average_note < 0 || values.average_note > 5) { 82 | errors.average_note = 'Should be between 0 and 5'; 83 | } 84 | 85 | return errors; 86 | }} 87 | > 88 | 89 | 90 | 91 | 92 | {({ values }) => 93 | values.title ? ( 94 | 95 | ) : null 96 | } 97 | 98 | 99 | 103 | 104 | 109 | 110 | 111 | 112 | 113 | 114 | {permissions === 'admin' && ( 115 | 116 | 117 | 122 | 123 | 124 | 125 | {({ 126 | formData, 127 | scopedFormData, 128 | getSource, 129 | ...rest 130 | }) => 131 | scopedFormData && scopedFormData.user_id ? ( 132 | 151 | ) : null 152 | } 153 | 154 | 155 | 156 | )} 157 | 158 | 159 | ); 160 | }; 161 | 162 | export default PostCreate; 163 | -------------------------------------------------------------------------------- /src/posts/PostEdit.js: -------------------------------------------------------------------------------- 1 | import RichTextInput from 'ra-input-rich-text'; 2 | import * as React from 'react'; 3 | import { 4 | TopToolbar, 5 | AutocompleteInput, 6 | ArrayInput, 7 | BooleanInput, 8 | CheckboxGroupInput, 9 | Datagrid, 10 | DateField, 11 | DateInput, 12 | Edit, 13 | CloneButton, 14 | ShowButton, 15 | EditButton, 16 | FormTab, 17 | ImageField, 18 | ImageInput, 19 | NumberInput, 20 | ReferenceManyField, 21 | ReferenceInput, 22 | SelectInput, 23 | SimpleFormIterator, 24 | TabbedForm, 25 | TextField, 26 | TextInput, 27 | minValue, 28 | number, 29 | required, 30 | FormDataConsumer, 31 | } from 'react-admin'; // eslint-disable-line import/no-unresolved 32 | import PostTitle from './PostTitle'; 33 | import TagReferenceInput from './TagReferenceInput'; 34 | 35 | const EditActions = ({ basePath, data, hasShow }) => ( 36 | 37 | 42 | {hasShow && } 43 | 44 | ); 45 | 46 | const PostEdit = ({ permissions, ...props }) => ( 47 | } actions={} {...props}> 48 | 49 | 50 | 51 | 52 | 59 | 67 | 68 | 69 | 70 | {permissions === 'admin' && ( 71 | 72 | 73 | 78 | 79 | 80 | 81 | {({ 82 | formData, 83 | scopedFormData, 84 | getSource, 85 | ...rest 86 | }) => 87 | scopedFormData && scopedFormData.user_id ? ( 88 | 107 | ) : null 108 | } 109 | 110 | 111 | 112 | )} 113 | 114 | 115 | 121 | 122 | 123 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 144 | 148 | 149 | 150 | 151 | 152 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | ); 169 | 170 | export default PostEdit; 171 | -------------------------------------------------------------------------------- /src/posts/PostList.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Children, Fragment, cloneElement, memo } from 'react'; 3 | import BookIcon from '@material-ui/icons/Book'; 4 | import Chip from '@material-ui/core/Chip'; 5 | import { useMediaQuery, makeStyles } from '@material-ui/core'; 6 | import lodashGet from 'lodash/get'; 7 | import jsonExport from 'jsonexport/dist'; 8 | import { 9 | BooleanField, 10 | BulkDeleteButton, 11 | BulkExportButton, 12 | ChipField, 13 | Datagrid, 14 | DateField, 15 | downloadCSV, 16 | EditButton, 17 | Filter, 18 | List, 19 | NumberField, 20 | ReferenceArrayField, 21 | SearchInput, 22 | ShowButton, 23 | SimpleList, 24 | SingleFieldList, 25 | TextField, 26 | TextInput, 27 | useTranslate, 28 | } from 'react-admin'; // eslint-disable-line import/no-unresolved 29 | 30 | import ResetViewsButton from './ResetViewsButton'; 31 | export const PostIcon = BookIcon; 32 | 33 | const useQuickFilterStyles = makeStyles(theme => ({ 34 | chip: { 35 | marginBottom: theme.spacing(1), 36 | }, 37 | })); 38 | const QuickFilter = ({ label }) => { 39 | const translate = useTranslate(); 40 | const classes = useQuickFilterStyles(); 41 | return ; 42 | }; 43 | 44 | const PostFilter = props => ( 45 | 46 | 47 | 51 | 56 | 57 | ); 58 | 59 | const exporter = posts => { 60 | const data = posts.map(post => ({ 61 | ...post, 62 | backlinks: lodashGet(post, 'backlinks', []).map( 63 | backlink => backlink.url 64 | ), 65 | })); 66 | jsonExport(data, (err, csv) => downloadCSV(csv, 'posts')); 67 | }; 68 | 69 | const useStyles = makeStyles(theme => ({ 70 | title: { 71 | maxWidth: '20em', 72 | overflow: 'hidden', 73 | textOverflow: 'ellipsis', 74 | whiteSpace: 'nowrap', 75 | }, 76 | hiddenOnSmallScreens: { 77 | [theme.breakpoints.down('md')]: { 78 | display: 'none', 79 | }, 80 | }, 81 | publishedAt: { fontStyle: 'italic' }, 82 | })); 83 | 84 | const PostListBulkActions = memo(props => ( 85 | 86 | 87 | 88 | 89 | 90 | )); 91 | 92 | const usePostListActionToolbarStyles = makeStyles({ 93 | toolbar: { 94 | alignItems: 'center', 95 | display: 'flex', 96 | marginTop: -1, 97 | marginBottom: -1, 98 | }, 99 | }); 100 | 101 | const PostListActionToolbar = ({ children, ...props }) => { 102 | const classes = usePostListActionToolbarStyles(); 103 | return ( 104 |
105 | {Children.map(children, button => cloneElement(button, props))} 106 |
107 | ); 108 | }; 109 | 110 | const rowClick = (id, basePath, record) => { 111 | if (record.commentable) { 112 | return 'edit'; 113 | } 114 | 115 | return 'show'; 116 | }; 117 | 118 | const PostPanel = ({ id, record, resource }) => ( 119 |
120 | ); 121 | 122 | const PostList = props => { 123 | const classes = useStyles(); 124 | const isSmall = useMediaQuery(theme => theme.breakpoints.down('sm')); 125 | return ( 126 | } 129 | filters={} 130 | sort={{ field: 'published_at', order: 'DESC' }} 131 | exporter={exporter} 132 | > 133 | {isSmall ? ( 134 | record.title} 136 | secondaryText={record => `${record.views} views`} 137 | tertiaryText={record => 138 | new Date(record.published_at).toLocaleDateString() 139 | } 140 | /> 141 | ) : ( 142 | 143 | 144 | 145 | 150 | 151 | 156 | 157 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | )} 175 | 176 | ); 177 | }; 178 | 179 | export default PostList; 180 | -------------------------------------------------------------------------------- /src/posts/PostShow.js: -------------------------------------------------------------------------------- 1 | import { useShowController } from 'ra-core'; 2 | import * as React from 'react'; 3 | import { 4 | ArrayField, 5 | BooleanField, 6 | CloneButton, 7 | ChipField, 8 | Datagrid, 9 | DateField, 10 | EditButton, 11 | NumberField, 12 | ReferenceArrayField, 13 | ReferenceManyField, 14 | RichTextField, 15 | SelectField, 16 | ShowView, 17 | SingleFieldList, 18 | Tab, 19 | TabbedShowLayout, 20 | TextField, 21 | UrlField, 22 | } from 'react-admin'; // eslint-disable-line import/no-unresolved 23 | import { Link } from 'react-router-dom'; 24 | import Button from '@material-ui/core/Button'; 25 | import PostTitle from './PostTitle'; 26 | 27 | const CreateRelatedComment = ({ record }) => ( 28 | 37 | ); 38 | 39 | const PostShow = props => { 40 | const controllerProps = useShowController(props); 41 | return ( 42 | }> 43 | 44 | 45 | 46 | 47 | {controllerProps.record && 48 | controllerProps.record.title === 49 | 'Fusce massa lorem, pulvinar a posuere ut, accumsan ac nisi' && ( 50 | 51 | )} 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | ); 105 | }; 106 | 107 | export default PostShow; 108 | -------------------------------------------------------------------------------- /src/posts/PostTitle.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useTranslate } from 'react-admin'; 3 | 4 | export default ({ record }) => { 5 | const translate = useTranslate(); 6 | return ( 7 | 8 | {record 9 | ? translate('post.edit.title', { title: record.title }) 10 | : ''} 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /src/posts/ResetViewsButton.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import VisibilityOff from '@material-ui/icons/VisibilityOff'; 4 | import { 5 | useUpdateMany, 6 | useRefresh, 7 | useNotify, 8 | useUnselectAll, 9 | Button, 10 | CRUD_UPDATE_MANY, 11 | } from 'react-admin'; 12 | 13 | const ResetViewsButton = ({ resource, selectedIds }) => { 14 | const notify = useNotify(); 15 | const unselectAll = useUnselectAll(); 16 | const refresh = useRefresh(); 17 | const [updateMany, { loading }] = useUpdateMany( 18 | resource, 19 | selectedIds, 20 | { views: 0 }, 21 | { 22 | action: CRUD_UPDATE_MANY, 23 | onSuccess: () => { 24 | notify( 25 | 'ra.notification.updated', 26 | 'info', 27 | { smart_count: selectedIds.length }, 28 | true 29 | ); 30 | unselectAll(resource); 31 | refresh(); 32 | }, 33 | onFailure: error => 34 | notify( 35 | typeof error === 'string' 36 | ? error 37 | : error.message || 'ra.notification.http_error', 38 | 'warning' 39 | ), 40 | undoable: true, 41 | } 42 | ); 43 | 44 | return ( 45 | 52 | ); 53 | }; 54 | 55 | ResetViewsButton.propTypes = { 56 | basePath: PropTypes.string, 57 | label: PropTypes.string, 58 | resource: PropTypes.string.isRequired, 59 | selectedIds: PropTypes.arrayOf(PropTypes.any).isRequired, 60 | }; 61 | 62 | export default ResetViewsButton; 63 | -------------------------------------------------------------------------------- /src/posts/TagReferenceInput.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useState } from 'react'; 3 | import { useForm } from 'react-final-form'; 4 | import { AutocompleteArrayInput, ReferenceArrayInput } from 'react-admin'; 5 | import Button from '@material-ui/core/Button'; 6 | import { makeStyles } from '@material-ui/core/styles'; 7 | 8 | const useStyles = makeStyles({ 9 | button: { 10 | margin: '0 24px', 11 | position: 'relative', 12 | }, 13 | input: { 14 | display: 'flex', 15 | flexDirection: 'row', 16 | justifyContent: 'flex-start', 17 | width: '50%', 18 | }, 19 | }); 20 | 21 | const TagReferenceInput = ({ ...props }) => { 22 | const classes = useStyles(); 23 | const { change } = useForm(); 24 | const [filter, setFilter] = useState(true); 25 | 26 | const handleAddFilter = () => { 27 | setFilter(!filter); 28 | change('tags', []); 29 | }; 30 | 31 | return ( 32 |
33 | 34 | 35 | 36 | 43 |
44 | ); 45 | }; 46 | 47 | export default TagReferenceInput; 48 | -------------------------------------------------------------------------------- /src/posts/index.js: -------------------------------------------------------------------------------- 1 | import BookIcon from '@material-ui/icons/Book'; 2 | import PostCreate from './PostCreate'; 3 | import PostEdit from './PostEdit'; 4 | import PostList from './PostList'; 5 | import PostShow from './PostShow'; 6 | 7 | export default { 8 | list: PostList, 9 | create: PostCreate, 10 | edit: PostEdit, 11 | show: PostShow, 12 | icon: BookIcon, 13 | }; 14 | -------------------------------------------------------------------------------- /src/tags/TagCreate.js: -------------------------------------------------------------------------------- 1 | /* eslint react/jsx-key: off */ 2 | import * as React from 'react'; 3 | import { 4 | Create, 5 | SimpleForm, 6 | TextField, 7 | TextInput, 8 | required, 9 | } from 'react-admin'; 10 | 11 | const TagCreate = props => ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | 20 | export default TagCreate; 21 | -------------------------------------------------------------------------------- /src/tags/TagEdit.js: -------------------------------------------------------------------------------- 1 | /* eslint react/jsx-key: off */ 2 | import * as React from 'react'; 3 | import { Edit, SimpleForm, TextField, TextInput, required } from 'react-admin'; 4 | 5 | const TagEdit = props => ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | 14 | export default TagEdit; 15 | -------------------------------------------------------------------------------- /src/tags/TagList.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Fragment, useState } from 'react'; 3 | import { List, EditButton } from 'react-admin'; 4 | import { 5 | List as MuiList, 6 | ListItem, 7 | ListItemText, 8 | ListItemSecondaryAction, 9 | Collapse, 10 | Card, 11 | makeStyles, 12 | } from '@material-ui/core'; 13 | import ExpandLess from '@material-ui/icons/ExpandLess'; 14 | import ExpandMore from '@material-ui/icons/ExpandMore'; 15 | 16 | const useStyles = makeStyles({ 17 | card: { 18 | maxWidth: '20em', 19 | marginTop: '1em', 20 | }, 21 | }); 22 | const SmallCard = ({ className, ...props }) => { 23 | const classes = useStyles(); 24 | return ; 25 | }; 26 | 27 | const SubTree = ({ level, root, getChildNodes, openChildren, toggleNode }) => { 28 | const childNodes = getChildNodes(root); 29 | const hasChildren = childNodes.length > 0; 30 | const open = openChildren.includes(root.id); 31 | return ( 32 | 33 | hasChildren && toggleNode(root)} 36 | style={{ paddingLeft: level * 16 }} 37 | > 38 | {hasChildren && open && } 39 | {hasChildren && !open && } 40 | {!hasChildren &&
 
} 41 | 42 | 43 | 44 | 45 | 46 |
47 | 48 | 49 | {childNodes.map(node => ( 50 | 58 | ))} 59 | 60 | 61 |
62 | ); 63 | }; 64 | 65 | const Tree = ({ ids, data }) => { 66 | const [openChildren, setOpenChildren] = useState([]); 67 | const toggleNode = node => 68 | setOpenChildren(state => { 69 | if (state.includes(node.id)) { 70 | return [ 71 | ...state.splice(0, state.indexOf(node.id)), 72 | ...state.splice(state.indexOf(node.id) + 1, state.length), 73 | ]; 74 | } else { 75 | return [...state, node.id]; 76 | } 77 | }); 78 | const nodes = ids.map(id => data[id]); 79 | const roots = nodes.filter(node => typeof node.parent_id === 'undefined'); 80 | const getChildNodes = root => 81 | nodes.filter(node => node.parent_id === root.id); 82 | return ( 83 | 84 | {roots.map(root => ( 85 | 93 | ))} 94 | 95 | ); 96 | }; 97 | 98 | const TagList = props => ( 99 | 106 | 107 | 108 | ); 109 | 110 | export default TagList; 111 | -------------------------------------------------------------------------------- /src/tags/TagShow.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Show, SimpleShowLayout, TextField } from 'react-admin'; // eslint-disable-line import/no-unresolved 3 | 4 | const TagShow = props => ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | ); 12 | 13 | export default TagShow; 14 | -------------------------------------------------------------------------------- /src/tags/index.js: -------------------------------------------------------------------------------- 1 | import TagCreate from './TagCreate'; 2 | import TagEdit from './TagEdit'; 3 | import TagList from './TagList'; 4 | import TagShow from './TagShow'; 5 | 6 | export default { 7 | create: TagCreate, 8 | edit: TagEdit, 9 | list: TagList, 10 | show: TagShow, 11 | }; 12 | -------------------------------------------------------------------------------- /src/theme.js: -------------------------------------------------------------------------------- 1 | import { defaultTheme } from "react-admin"; 2 | import merge from "lodash/merge"; 3 | import createMuiTheme from "@material-ui/core/styles/createMuiTheme"; 4 | import createPalette from "@material-ui/core/styles/createPalette"; 5 | import defaultMuiTheme from "@material-ui/core/styles/defaultTheme"; 6 | 7 | const palette = createPalette( 8 | merge({}, defaultTheme.palette, { 9 | primary: { 10 | main: "#ff0266", 11 | }, 12 | secondary: { 13 | main: "#00ba00", 14 | }, 15 | }) 16 | ); 17 | 18 | const typography = { 19 | fontFamilySecondary: "'Poppins', sans-serif", 20 | fontFamily: '"Comic Neue", cursive', 21 | fontSize: 16, 22 | fontStyle: "normal", 23 | fontWeightLight: 400, 24 | fontWeightRegular: 500, 25 | fontWeightMedium: 600, 26 | fontWeightBold: 700, 27 | color: palette.text.primary, 28 | }; 29 | 30 | const typographyBase = { 31 | fontFamily: typography.fontFamily, 32 | fontSize: typography.fontSize, 33 | fontStyle: typography.fontStyle, 34 | color: typography.color, 35 | }; 36 | 37 | const typographyHeader = { 38 | ...typographyBase, 39 | fontWeight: typography.fontWeightBold, 40 | fontFamily: typography.fontFamilySecondary, 41 | }; 42 | 43 | const typographyBody = { 44 | ...typographyBase, 45 | fontWeight: typography.fontWeightRegular, 46 | fontFamily: typography.fontFamily, 47 | }; 48 | 49 | const rawTheme = { 50 | palette, 51 | 52 | typography: { 53 | ...typographyBase, 54 | h1: { 55 | ...typographyHeader, 56 | textTransform: "uppercase", 57 | fontSize: "4rem", 58 | }, 59 | h2: { 60 | ...typographyHeader, 61 | textTransform: "uppercase", 62 | fontSize: "3rem", 63 | }, 64 | h3: { 65 | ...typographyHeader, 66 | fontSize: "2.75rem", 67 | }, 68 | h4: { 69 | ...typographyHeader, 70 | fontSize: "2rem", 71 | }, 72 | h5: { 73 | ...typographyHeader, 74 | fontWeight: typography.fontWeightMedium, 75 | fontSize: "1.5rem", 76 | }, 77 | h6: { 78 | ...typographyHeader, 79 | fontWeight: typography.fontWeightMedium, 80 | fontSize: "1.25rem", 81 | }, 82 | body1: { 83 | ...typographyBody, 84 | fontSize: "1rem", 85 | }, 86 | body2: { 87 | ...typographyBody, 88 | fontSize: "1rem", 89 | }, 90 | button: { 91 | ...typographyBody, 92 | fontSize: "1rem", 93 | }, 94 | caption: { 95 | ...typographyBody, 96 | fontSize: "0.875rem", 97 | fontStyle: "italic", 98 | }, 99 | }, 100 | 101 | shape: { 102 | borderRadius: 0, 103 | }, 104 | 105 | overrides: { 106 | // React-Admin 107 | RaAppBar: { 108 | title: { 109 | textTransform: "capitalize", 110 | }, 111 | }, 112 | 113 | // Material-UI 114 | 115 | MuiAppBar: { 116 | root: { 117 | background: `linear-gradient(127deg, #00ff00, #00ba00);`, 118 | }, 119 | }, 120 | MuiCard: { 121 | root: { 122 | border: "none", 123 | }, 124 | }, 125 | MuiButton: { 126 | root: { 127 | color: palette.primary.main, 128 | paddingTop: defaultMuiTheme.spacing(1), 129 | paddingRight: defaultMuiTheme.spacing(4), 130 | paddingBottom: defaultMuiTheme.spacing(1), 131 | paddingLeft: defaultMuiTheme.spacing(4), 132 | }, 133 | sizeSmall: { 134 | paddingTop: defaultMuiTheme.spacing(0), 135 | paddingRight: defaultMuiTheme.spacing(2), 136 | paddingBottom: defaultMuiTheme.spacing(0), 137 | paddingLeft: defaultMuiTheme.spacing(2), 138 | }, 139 | sizeLarge: { 140 | paddingTop: defaultMuiTheme.spacing(2), 141 | paddingRight: defaultMuiTheme.spacing(6), 142 | paddingBottom: defaultMuiTheme.spacing(2), 143 | paddingLeft: defaultMuiTheme.spacing(6), 144 | }, 145 | contained: { 146 | boxShadow: "none", 147 | }, 148 | containedPrimary: { 149 | color: palette.common.white, 150 | backgroundColor: palette.primary.main, 151 | }, 152 | containedSecondary: { 153 | color: palette.common.white, 154 | backgroundColor: palette.secondary.main, 155 | }, 156 | outlined: {}, 157 | outlinedPrimary: { 158 | color: palette.primary.main, 159 | borderColor: palette.primary.main, 160 | }, 161 | outlinedSecondary: { 162 | color: palette.common.white, 163 | borderColor: palette.common.white, 164 | }, 165 | text: {}, 166 | textPrimary: { 167 | color: palette.primary.main, 168 | }, 169 | textSecondary: { 170 | color: palette.secondary.main, 171 | }, 172 | label: {}, 173 | }, 174 | 175 | // React-Admin 176 | 177 | RaSidebar: { 178 | drawerPaper: { 179 | backgroundColor: palette.common.white, 180 | color: palette.primary.main, 181 | height: "100%", 182 | boxShadow: 183 | "2px 0px 1px -1px rgba(0,0,0,0.2), 1px 0px 3px 0px rgba(0,0,0,0.1)", 184 | }, 185 | }, 186 | RaMenuItemLink: { 187 | active: { 188 | borderLeftStyle: "none", 189 | borderRightColor: palette.secondary.main, 190 | borderRightWidth: defaultMuiTheme.spacing(0.5), 191 | borderRightStyle: "solid", 192 | backgroundColor: palette.action.selected, 193 | color: palette.primary.main, 194 | }, 195 | icon: { 196 | color: "inherit", 197 | }, 198 | }, 199 | RaLayout: { 200 | content: { 201 | height: "auto", 202 | backgroundColor: palette.background.default, 203 | paddingTop: defaultMuiTheme.spacing(0), 204 | paddingRight: defaultMuiTheme.spacing(0), 205 | paddingBottom: defaultMuiTheme.spacing(0), 206 | paddingLeft: defaultMuiTheme.spacing(0), 207 | display: "flex", 208 | flexDirection: "column", 209 | [defaultMuiTheme.breakpoints.up("xs")]: { 210 | paddingTop: defaultMuiTheme.spacing(0), 211 | paddingRight: defaultMuiTheme.spacing(0), 212 | paddingBottom: defaultMuiTheme.spacing(0), 213 | paddingLeft: defaultMuiTheme.spacing(0), 214 | }, 215 | "& > div, & > h2": { 216 | paddingTop: defaultMuiTheme.spacing(4), 217 | paddingRight: defaultMuiTheme.spacing(3), 218 | paddingBottom: defaultMuiTheme.spacing(3), 219 | paddingLeft: defaultMuiTheme.spacing(3), 220 | [defaultMuiTheme.breakpoints.up("xs")]: { 221 | paddingLeft: defaultMuiTheme.spacing(6), 222 | }, 223 | }, 224 | }, 225 | }, 226 | RaAppBar: { 227 | toolbar: { 228 | MuiIconButton: { 229 | root: { 230 | fontSize: "1.25rem", 231 | }, 232 | }, 233 | }, 234 | }, 235 | RaTabbedShowLayout: { 236 | content: { 237 | marginTop: defaultMuiTheme.spacing(4), 238 | backgroundColor: palette.common.white, 239 | boxShadow: defaultMuiTheme.shadows[3], 240 | paddingTop: 0, 241 | paddingBottom: 0, 242 | paddingLeft: 0, 243 | paddingRight: 0, 244 | }, 245 | }, 246 | RaShow: { 247 | main: { 248 | marginTop: defaultMuiTheme.spacing(2), 249 | }, 250 | noActions: { 251 | marginTop: defaultMuiTheme.spacing(2), 252 | }, 253 | }, 254 | RaFilter: { 255 | form: {}, 256 | button: { 257 | "& button": { 258 | borderStyle: "solid", 259 | borderWidth: "2px", 260 | borderColor: palette.grey[300], 261 | textTransform: "uppercase", 262 | fontWeight: typography.fontWeightBold, 263 | color: palette.primary.main, 264 | paddingLeft: defaultMuiTheme.spacing(2), 265 | paddingRight: defaultMuiTheme.spacing(2), 266 | paddingTop: defaultMuiTheme.spacing(1), 267 | paddingBottom: defaultMuiTheme.spacing(1), 268 | }, 269 | }, 270 | }, 271 | RaListToolbar: { 272 | toolbar: { 273 | paddingBottom: defaultMuiTheme.spacing(1), 274 | borderBottomStyle: "solid", 275 | borderBottomWidth: "1px", 276 | borderBottomColor: palette.grey[300], 277 | marginBottom: defaultMuiTheme.spacing(2), 278 | }, 279 | actions: { 280 | marginRight: "0px", 281 | }, 282 | }, 283 | }, 284 | }; 285 | 286 | export const theme = createMuiTheme(merge({}, defaultTheme, rawTheme)); 287 | -------------------------------------------------------------------------------- /src/users/Aside.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Typography from '@material-ui/core/Typography'; 3 | import { makeStyles } from '@material-ui/core/styles'; 4 | 5 | const useStyles = makeStyles(theme => ({ 6 | root: { 7 | [theme.breakpoints.up('sm')]: { 8 | width: 200, 9 | margin: '1em', 10 | }, 11 | [theme.breakpoints.down('sm')]: { 12 | width: 0, 13 | overflowX: 'hidden', 14 | margin: 0, 15 | }, 16 | }, 17 | })); 18 | 19 | const Aside = () => { 20 | const classes = useStyles(); 21 | return ( 22 |
23 | App Users 24 | 25 | Eiusmod adipisicing tempor duis qui. Ullamco aliqua tempor 26 | incididunt aliquip aliquip qui ad minim aliqua. Aute et magna 27 | quis pariatur irure sunt. Aliquip velit consequat dolore ullamco 28 | laborum voluptate cupidatat. Proident minim reprehenderit id 29 | dolore elit sit occaecat ad amet tempor esse occaecat enim. 30 | Laborum aliqua excepteur qui ipsum in dolor et cillum est. 31 | 32 |
33 | ); 34 | }; 35 | 36 | export default Aside; 37 | -------------------------------------------------------------------------------- /src/users/UserCreate.js: -------------------------------------------------------------------------------- 1 | /* eslint react/jsx-key: off */ 2 | import * as React from 'react'; 3 | import { 4 | Create, 5 | FormTab, 6 | SaveButton, 7 | AutocompleteInput, 8 | TabbedForm, 9 | TextInput, 10 | Toolbar, 11 | required, 12 | } from 'react-admin'; 13 | 14 | import Aside from './Aside'; 15 | 16 | const UserEditToolbar = ({ 17 | permissions, 18 | hasList, 19 | hasEdit, 20 | hasShow, 21 | hasCreate, 22 | ...props 23 | }) => ( 24 | 25 | 30 | {permissions === 'admin' && ( 31 | 37 | )} 38 | 39 | ); 40 | 41 | const UserCreate = ({ permissions, ...props }) => ( 42 | }> 43 | } 45 | > 46 | 47 | 53 | 54 | {permissions === 'admin' && ( 55 | 56 | 65 | 66 | )} 67 | 68 | 69 | ); 70 | 71 | export default UserCreate; 72 | -------------------------------------------------------------------------------- /src/users/UserEdit.js: -------------------------------------------------------------------------------- 1 | /* eslint react/jsx-key: off */ 2 | import * as React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import { 5 | CloneButton, 6 | DeleteWithConfirmButton, 7 | Edit, 8 | FormTab, 9 | required, 10 | SaveButton, 11 | SelectInput, 12 | ShowButton, 13 | TabbedForm, 14 | TextInput, 15 | Toolbar, 16 | TopToolbar, 17 | } from 'react-admin'; 18 | import { makeStyles } from '@material-ui/core/styles'; 19 | 20 | import UserTitle from './UserTitle'; 21 | import Aside from './Aside'; 22 | 23 | const useToolbarStyles = makeStyles({ 24 | toolbar: { 25 | display: 'flex', 26 | justifyContent: 'space-between', 27 | }, 28 | }); 29 | 30 | /** 31 | * Custom Toolbar for the Edit form 32 | * 33 | * Save with undo, but delete with confirm 34 | */ 35 | const UserEditToolbar = props => { 36 | const classes = useToolbarStyles(); 37 | return ( 38 | 39 | 40 | 41 | 42 | ); 43 | }; 44 | 45 | const EditActions = ({ basePath, data, hasShow }) => ( 46 | 47 | 52 | 53 | 54 | ); 55 | 56 | const UserEdit = ({ permissions, ...props }) => ( 57 | } 59 | aside={