├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── example ├── .gitignore ├── README.md ├── babel.config.js ├── package.json ├── src │ ├── Layout.js │ ├── addUploadFeature.js │ ├── authProvider.js │ ├── comments │ │ ├── CommentCreate.js │ │ ├── CommentDeleteConfirm.js │ │ ├── CommentEdit.js │ │ ├── CommentList.js │ │ ├── CommentShow.js │ │ ├── PostPreview.js │ │ ├── PostQuickCreate.js │ │ ├── PostQuickCreateCancelButton.js │ │ ├── PostReferenceInput.js │ │ └── index.js │ ├── customRouteLayout.js │ ├── customRouteNoLayout.js │ ├── data.js │ ├── dataProvider.js │ ├── i18n │ │ ├── en.js │ │ ├── fr.js │ │ └── index.js │ ├── i18nProvider.js │ ├── index.html │ ├── index.js │ ├── posts │ │ ├── PostCreate.js │ │ ├── PostDeleteConfirm.js │ │ ├── PostEdit.js │ │ ├── PostList.js │ │ ├── PostShow.js │ │ ├── PostTitle.js │ │ ├── ResetViewsButton.js │ │ └── index.js │ ├── tags │ │ ├── TagCreate.js │ │ ├── TagDeleteConfirm.js │ │ ├── TagEdit.js │ │ ├── TagList.js │ │ ├── TagShow.js │ │ └── index.js │ └── validators.js └── webpack.config.js ├── img └── ra-delete-with-custom-confirm-button.gif ├── lib ├── DeleteWithCustomConfirmButton.js └── index.js ├── package-lock.json ├── package.json └── src ├── DeleteWithCustomConfirmButton.js └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/react" 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-proposal-class-properties" 8 | ] 9 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true 4 | }, 5 | "extends": [ 6 | "plugin:react/recommended" 7 | ], 8 | "globals": { 9 | "Atomics": "readonly", 10 | "SharedArrayBuffer": "readonly" 11 | }, 12 | "parser": "babel-eslint", 13 | "parserOptions": { 14 | "ecmaFeatures": { 15 | "jsx": true 16 | }, 17 | "ecmaVersion": 6, 18 | "sourceType": "module" 19 | }, 20 | "plugins": [ 21 | "react" 22 | ], 23 | "rules": { 24 | "react/forbid-prop-types": ["off"], 25 | "react/prop-types": ["warn"], 26 | "react/jsx-no-bind": ["off"], 27 | "react/jsx-indent": ["off"], 28 | "react/jsx-indent-props": ["off"], 29 | "react/jsx-filename-extension": ["off"], 30 | "import/no-named-as-default": ["off"], 31 | "no-unused-vars": [ 32 | "error", 33 | { 34 | "ignoreRestSiblings": true 35 | } 36 | ] 37 | }, 38 | "settings": { 39 | "react": { 40 | "version": "detect" 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /.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 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | img/ 3 | node_modules/ 4 | src/ 5 | example/ 6 | .babelrc 7 | .eslintrc 8 | .gitignore 9 | .travis.yml 10 | 11 | # Output of 'npm pack' 12 | *.tgz 13 | 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | stages: 4 | # - test 5 | - build 6 | 7 | jobs: 8 | include: 9 | # - name: Test with Node.js lts/* 10 | # stage: test 11 | # node_js: "lts/*" 12 | # script: 13 | # - npm test && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 14 | - name: Build with Node.js lts/* 15 | stage: build 16 | node_js: "lts/*" 17 | script: 18 | - npm run build 19 | - name: Build with Node.js stable 20 | stage: build 21 | node_js: "stable" 22 | script: 23 | - npm run build 24 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.formatOnSave": true, 4 | "editor.formatOnPaste": true, 5 | "editor.formatOnType": true 6 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 itTkm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ra-delete-with-custom-confirm-button 2 | 3 | [![npm version](https://img.shields.io/npm/v/ra-delete-with-custom-confirm-button.svg)](https://www.npmjs.com/package/ra-delete-with-custom-confirm-button) 4 | [![npm downloads](https://img.shields.io/npm/dt/ra-delete-with-custom-confirm-button)](https://www.npmjs.com/package/ra-delete-with-custom-confirm-button) 5 | [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](./LICENSE) 6 | [![Build Status](https://travis-ci.com/itTkm/ra-delete-with-custom-confirm-button.svg?branch=master)](https://travis-ci.com/itTkm/ra-delete-with-custom-confirm-button) 7 | 8 | Delete button with your custom confirm dialog for [React-admin](https://marmelab.com/react-admin/). 9 | 10 | ![Demo](img/ra-delete-with-custom-confirm-button.gif?raw=true "Demo") 11 | 12 | ## Installation 13 | 14 | ```bash 15 | # via npm 16 | npm install --save ra-delete-with-custom-confirm-button 17 | 18 | # via yarn 19 | yarn add ra-delete-with-custom-confirm-button 20 | ``` 21 | 22 | ## Demo 23 | 24 | After having cloned this repository, run the following commands: 25 | 26 | ```bash 27 | cd example/ 28 | yarn install 29 | yarn start 30 | ``` 31 | 32 | And then browse to [http://localhost:8080/](http://localhost:8080/). 33 | 34 | The credentials are _login/password_ 35 | 36 | ## Usage 37 | 38 | ```js 39 | import DeleteWithCustomConfirmButton from "ra-delete-with-custom-confirm-button"; 40 | import Delete from "@material-ui/icons/Delete"; 41 | import ErrorOutline from "@material-ui/icons/ErrorOutline"; 42 | 43 | // Define your custom title of confirm dialog 44 | const DeleteConfirmTitle = "Are you sure you want to delete this post?"; 45 | 46 | // Define your custom contents of confirm dialog 47 | const DeleteConfirmContent = (props) => { 48 | return ( 49 | 50 | 51 | 52 | 53 | 54 | 55 | ); 56 | }; 57 | 58 | const InformationList = (props) => { 59 | const translate = useTranslate(); 60 | return ( 61 | 62 | 63 | 64 | 65 | 66 | 76 | 77 | 78 | ); 79 | }; 80 | 81 | export default InformationList; 82 | ``` 83 | 84 | ## props 85 | 86 | | Name | Type | Description | Default | 87 | | ------------ | ------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 88 | | title | string | your custom title of delete confirm dialog | 89 | | content | element | your custom contents of delete confirm dialog | 90 | | label | string | label of delete button | 'ra.action.delete' (`Delete` in English) | 91 | | confirmColor | string | color of delete button ('warning' or 'primary') | 'warning' | 92 | | DeleteIcon | element | icon of delete button from [@material-ui/icons](https://www.npmjs.com/package/@material-ui/icons) | ![Delete](https://github.com/google/material-design-icons/blob/master/action/drawable-hdpi/ic_delete_black_18dp.png?raw=true "import Delete from '@material-ui/icons/Delete';") | 93 | | cancel | string | label of cancel button | 'ra.action.cancel' (`Cancel` in English) | 94 | | CancelIcon | element | icon of cancel button from [@material-ui/icons](https://www.npmjs.com/package/@material-ui/icons) | ![ErrorOutline](https://github.com/google/material-design-icons/blob/master/alert/drawable-hdpi/ic_error_outline_black_18dp.png?raw=true "import ErrorOutline from '@material-ui/icons/ErrorOutline';") | 95 | | undoable | bool | undoable or not | true | 96 | | redirect | string | redirect to | 'list' | 97 | 98 | ## License 99 | 100 | This library is licensed under the [MIT License](./LICENSE). 101 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | yarn.lock 2 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Sample of ra-delete-with-custom-confirm-button 2 | 3 | This is the sample application of `ra-delete-with-custom-confirm-button`. 4 | It is based [React-admin simple](https://github.com/marmelab/react-admin/tree/master/examples/simple) project. 5 | 6 | ## How to run 7 | 8 | After having cloned the ra-delete-with-custom-confirm-button repository, run the following commands: 9 | 10 | ```sh 11 | yarn install 12 | yarn start 13 | ``` 14 | 15 | And then browse to [http://localhost:8080/](http://localhost:8080/). 16 | 17 | The credentials are **login/password** 18 | -------------------------------------------------------------------------------- /example/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 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple", 3 | "private": true, 4 | "version": "3.0.0", 5 | "description": "", 6 | "main": "index.html", 7 | "scripts": { 8 | "start": "./node_modules/.bin/webpack-dev-server --progress --color --hot --watch --mode development", 9 | "serve": "./node_modules/.bin/serve --listen 8080 ./dist", 10 | "build": "./node_modules/.bin/webpack-cli --color --mode development --hide-modules true" 11 | }, 12 | "author": "", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@babel/cli": "^7.1.2", 16 | "@babel/core": "^7.1.2", 17 | "@babel/plugin-proposal-class-properties": "^7.1.0", 18 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0", 19 | "@babel/plugin-syntax-dynamic-import": "^7.0.0", 20 | "@babel/preset-env": "^7.1.0", 21 | "@babel/preset-react": "^7.0.0", 22 | "@babel/preset-typescript": "^7.1.0", 23 | "babel-loader": "^8.0.4", 24 | "hard-source-webpack-plugin": "^0.11.2", 25 | "html-loader": "~0.5.5", 26 | "html-webpack-plugin": "~3.2.0", 27 | "ignore-not-found-export-plugin": "^1.0.1", 28 | "serve": "~11.3.2", 29 | "style-loader": "~0.20.3", 30 | "wait-on": "^3.2.0", 31 | "webpack": "~4.5.0", 32 | "webpack-bundle-analyzer": "^3.3.2", 33 | "webpack-cli": "~2.0.13", 34 | "webpack-dev-server": "~3.1.11" 35 | }, 36 | "dependencies": { 37 | "@babel/polyfill": "^7.0.0", 38 | "@material-ui/core": "^4.9.0", 39 | "@material-ui/icons": "^4.5.1", 40 | "ra-custom-confirm": "^1.1.1", 41 | "ra-data-fakerest": "^3.0.0", 42 | "ra-delete-with-custom-confirm-button": "^2.0.1", 43 | "ra-i18n-polyglot": "^3.0.0", 44 | "ra-input-rich-text": "^3.0.0", 45 | "ra-language-english": "^3.0.0", 46 | "ra-language-french": "^3.0.0", 47 | "react": "^16.9.0", 48 | "react-admin": "^3.0.0", 49 | "react-dom": "^16.9.0" 50 | } 51 | } -------------------------------------------------------------------------------- /example/src/Layout.js: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react'; 2 | import { Layout, AppBar, UserMenu, useLocale, useSetLocale } from 'react-admin'; 3 | import { makeStyles, MenuItem, ListItemIcon } from '@material-ui/core'; 4 | import Language from '@material-ui/icons/Language'; 5 | 6 | const useStyles = makeStyles(theme => ({ 7 | menuItem: { 8 | color: theme.palette.text.secondary, 9 | }, 10 | icon: { minWidth: theme.spacing(5) }, 11 | })); 12 | 13 | const SwitchLanguage = forwardRef((props, ref) => { 14 | const locale = useLocale(); 15 | const setLocale = useSetLocale(); 16 | const classes = useStyles(); 17 | return ( 18 | { 22 | setLocale(locale === 'en' ? 'fr' : 'en'); 23 | props.onClick(); 24 | }} 25 | > 26 | 27 | 28 | 29 | Switch Language 30 | 31 | ); 32 | }); 33 | 34 | const MyUserMenu = props => ( 35 | 36 | 37 | 38 | ); 39 | 40 | const MyAppBar = props => } />; 41 | 42 | export default props => ; 43 | -------------------------------------------------------------------------------- /example/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 | -------------------------------------------------------------------------------- /example/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 | -------------------------------------------------------------------------------- /example/src/comments/CommentCreate.js: -------------------------------------------------------------------------------- 1 | import 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 | 18 | 19 | 27 | 32 | 33 | 34 | 35 | 36 | ); 37 | 38 | export default CommentCreate; 39 | -------------------------------------------------------------------------------- /example/src/comments/CommentDeleteConfirm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | DateField, 4 | ReferenceField, 5 | SimpleShowLayout, 6 | TextField, 7 | } from 'react-admin'; 8 | 9 | // Define your custom title of confirm dialog 10 | const DeleteConfirmTitle = "Are you sure you want to delete this comment?"; 11 | 12 | // Define your custom contents of confirm dialog 13 | const DeleteConfirmContent = props => { 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | }; 26 | 27 | export { DeleteConfirmTitle, DeleteConfirmContent }; 28 | -------------------------------------------------------------------------------- /example/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 React from 'react'; 5 | import { 6 | AutocompleteInput, 7 | DateInput, 8 | EditActions, 9 | useEditController, 10 | Link, 11 | ReferenceInput, 12 | SaveButton, 13 | SimpleForm, 14 | TextInput, 15 | Title, 16 | Toolbar, 17 | minLength, 18 | } from 'react-admin'; // eslint-disable-line import/no-unresolved 19 | 20 | import DeleteWithCustomConfirmButton from "ra-delete-with-custom-confirm-button"; 21 | import { 22 | DeleteConfirmTitle, 23 | DeleteConfirmContent 24 | } from './CommentDeleteConfirm'; 25 | 26 | const useToolbarStyles = makeStyles({ 27 | toolbar: { 28 | display: 'flex', 29 | justifyContent: 'space-between', 30 | }, 31 | }); 32 | 33 | const LinkToRelatedPost = ({ record }) => ( 34 | 35 | 36 | See related post 37 | 38 | 39 | ); 40 | 41 | const useEditStyles = makeStyles({ 42 | actions: { 43 | float: 'right', 44 | }, 45 | card: { 46 | marginTop: '1em', 47 | maxWidth: '30em', 48 | }, 49 | }); 50 | 51 | const OptionRenderer = ({ record }) => ( 52 | 53 | {record.title} - {record.id} 54 | 55 | ); 56 | 57 | const inputText = record => `${record.title} - ${record.id}`; 58 | 59 | const CustomToolbar = props => { 60 | const classes = useToolbarStyles(); 61 | return ( 62 | 63 | 64 | 68 | 69 | ); 70 | }; 71 | 72 | const CommentEdit = props => { 73 | const classes = useEditStyles(); 74 | const { 75 | resource, 76 | record, 77 | redirect, 78 | save, 79 | basePath, 80 | version, 81 | } = useEditController(props); 82 | return ( 83 |
84 | 85 | <div className={classes.actions}> 86 | <EditActions 87 | basePath={basePath} 88 | resource={resource} 89 | data={record} 90 | hasShow 91 | hasList 92 | /> 93 | </div> 94 | <Card className={classes.card}> 95 | {record && ( 96 | <SimpleForm 97 | basePath={basePath} 98 | redirect={redirect} 99 | resource={resource} 100 | record={record} 101 | save={save} 102 | toolbar={<CustomToolbar />} 103 | version={version} 104 | > 105 | <TextInput disabled source="id" fullWidth /> 106 | <ReferenceInput 107 | source="post_id" 108 | reference="posts" 109 | perPage={15} 110 | sort={{ field: 'title', order: 'ASC' }} 111 | fullWidth 112 | > 113 | <AutocompleteInput 114 | matchSuggestion={(filterValue, suggestion) => 115 | true 116 | } 117 | optionText={<OptionRenderer />} 118 | inputText={inputText} 119 | options={{ fullWidth: true }} 120 | /> 121 | </ReferenceInput> 122 | 123 | <LinkToRelatedPost /> 124 | <TextInput 125 | source="author.name" 126 | validate={minLength(10)} 127 | fullWidth 128 | /> 129 | <DateInput source="created_at" fullWidth /> 130 | <TextInput 131 | source="body" 132 | validate={minLength(10)} 133 | fullWidth={true} 134 | multiline={true} 135 | /> 136 | </SimpleForm> 137 | )} 138 | </Card> 139 | </div> 140 | ); 141 | }; 142 | 143 | export default CommentEdit; 144 | -------------------------------------------------------------------------------- /example/src/comments/CommentList.js: -------------------------------------------------------------------------------- 1 | import 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 | -------------------------------------------------------------------------------- /example/src/comments/CommentShow.js: -------------------------------------------------------------------------------- 1 | import 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 | -------------------------------------------------------------------------------- /example/src/comments/PostPreview.js: -------------------------------------------------------------------------------- 1 | import 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 | -------------------------------------------------------------------------------- /example/src/comments/PostQuickCreate.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { useSelector, useDispatch } from 'react-redux'; 4 | import { makeStyles } from '@material-ui/core/styles'; 5 | import { 6 | CREATE, 7 | SaveButton, 8 | SimpleForm, 9 | TextInput, 10 | Toolbar, 11 | required, 12 | showNotification, 13 | } from 'react-admin'; // eslint-disable-line import/no-unresolved 14 | 15 | import CancelButton from './PostQuickCreateCancelButton'; 16 | 17 | // We need a custom toolbar to add our custom buttons 18 | // The CancelButton allows to close the modal without submitting anything 19 | const PostQuickCreateToolbar = ({ submitting, onCancel, ...props }) => ( 20 | <Toolbar {...props} disableGutters> 21 | <SaveButton /> 22 | <CancelButton onClick={onCancel} /> 23 | </Toolbar> 24 | ); 25 | 26 | PostQuickCreateToolbar.propTypes = { 27 | submitting: PropTypes.bool, 28 | onCancel: PropTypes.func.isRequired, 29 | }; 30 | 31 | const useStyles = makeStyles({ 32 | form: { padding: 0 }, 33 | }); 34 | 35 | const PostQuickCreate = ({ onCancel, onSave, ...props }) => { 36 | const classes = useStyles(); 37 | const dispatch = useDispatch(); 38 | const submitting = useSelector(state => state.admin.loading > 0); 39 | 40 | const handleSave = useCallback( 41 | values => { 42 | dispatch({ 43 | type: 'QUICK_CREATE', 44 | payload: { data: values }, 45 | meta: { 46 | fetch: CREATE, 47 | resource: 'posts', 48 | onSuccess: { 49 | callback: ({ payload: { data } }) => onSave(data), 50 | }, 51 | onFailure: { 52 | callback: ({ error }) => { 53 | dispatch(showNotification(error.message, 'error')); 54 | }, 55 | }, 56 | }, 57 | }); 58 | }, 59 | [dispatch, onSave] 60 | ); 61 | 62 | return ( 63 | <SimpleForm 64 | save={handleSave} 65 | saving={submitting} 66 | redirect={false} 67 | toolbar={ 68 | <PostQuickCreateToolbar 69 | onCancel={onCancel} 70 | submitting={submitting} 71 | /> 72 | } 73 | classes={{ form: classes.form }} 74 | {...props} 75 | > 76 | <TextInput source="title" validate={required()} /> 77 | <TextInput 78 | source="teaser" 79 | validate={required()} 80 | fullWidth={true} 81 | multiline={true} 82 | /> 83 | </SimpleForm> 84 | ); 85 | }; 86 | 87 | PostQuickCreate.propTypes = { 88 | onCancel: PropTypes.func.isRequired, 89 | onSave: PropTypes.func.isRequired, 90 | }; 91 | 92 | export default PostQuickCreate; 93 | -------------------------------------------------------------------------------- /example/src/comments/PostQuickCreateCancelButton.js: -------------------------------------------------------------------------------- 1 | import 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 | -------------------------------------------------------------------------------- /example/src/comments/PostReferenceInput.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useState, useCallback, useEffect } from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | import { FormSpy, useForm } from 'react-final-form'; 4 | 5 | import { makeStyles } from '@material-ui/core/styles'; 6 | import Button from '@material-ui/core/Button'; 7 | import Dialog from '@material-ui/core/Dialog'; 8 | import DialogTitle from '@material-ui/core/DialogTitle'; 9 | import DialogContent from '@material-ui/core/DialogContent'; 10 | import DialogActions from '@material-ui/core/DialogActions'; 11 | 12 | import { 13 | crudGetMatching, 14 | ReferenceInput, 15 | SelectInput, 16 | useTranslate, 17 | } from 'react-admin'; // eslint-disable-line import/no-unresolved 18 | 19 | import PostQuickCreate from './PostQuickCreate'; 20 | import PostPreview from './PostPreview'; 21 | 22 | const useStyles = makeStyles({ 23 | button: { 24 | margin: '10px 24px', 25 | position: 'relative', 26 | }, 27 | }); 28 | 29 | const PostReferenceInput = props => { 30 | const translate = useTranslate(); 31 | const classes = useStyles(); 32 | const dispatch = useDispatch(); 33 | const { change } = useForm(); 34 | 35 | const [showCreateDialog, setShowCreateDialog] = useState(false); 36 | const [showPreviewDialog, setShowPreviewDialog] = useState(false); 37 | const [newPostId, setNewPostId] = useState(''); 38 | 39 | useEffect(() => { 40 | //Refresh the choices of the ReferenceInput to ensure our newly created post 41 | // always appear, even after selecting another post 42 | dispatch( 43 | crudGetMatching( 44 | 'posts', 45 | 'comments@post_id', 46 | { page: 1, perPage: 25 }, 47 | { field: 'id', order: 'DESC' }, 48 | {} 49 | ) 50 | ); 51 | }, [dispatch, newPostId]); 52 | 53 | const handleNewClick = useCallback( 54 | event => { 55 | event.preventDefault(); 56 | setShowCreateDialog(true); 57 | }, 58 | [setShowCreateDialog] 59 | ); 60 | 61 | const handleShowClick = useCallback( 62 | event => { 63 | event.preventDefault(); 64 | setShowPreviewDialog(true); 65 | }, 66 | [setShowPreviewDialog] 67 | ); 68 | 69 | const handleCloseCreate = useCallback(() => { 70 | setShowCreateDialog(false); 71 | }, [setShowCreateDialog]); 72 | 73 | const handleCloseShow = useCallback(() => { 74 | setShowPreviewDialog(false); 75 | }, [setShowPreviewDialog]); 76 | 77 | const handleSave = useCallback( 78 | post => { 79 | setShowCreateDialog(false); 80 | setNewPostId(post.id); 81 | change('post_id', post.id); 82 | }, 83 | [setShowCreateDialog, setNewPostId, change] 84 | ); 85 | 86 | return ( 87 | <Fragment> 88 | <ReferenceInput {...props} defaultValue={newPostId}> 89 | <SelectInput optionText="title" /> 90 | </ReferenceInput> 91 | <Button 92 | data-testid="button-add-post" 93 | className={classes.button} 94 | onClick={handleNewClick} 95 | > 96 | {translate('ra.action.create')} 97 | </Button> 98 | <FormSpy 99 | subscription={{ values: true }} 100 | render={({ values }) => 101 | values.post_id ? ( 102 | <Fragment> 103 | <Button 104 | data-testid="button-show-post" 105 | className={classes.button} 106 | onClick={handleShowClick} 107 | > 108 | {translate('ra.action.show')} 109 | </Button> 110 | <Dialog 111 | data-testid="dialog-show-post" 112 | fullWidth 113 | open={showPreviewDialog} 114 | onClose={handleCloseShow} 115 | aria-label={translate('simple.create-post')} 116 | > 117 | <DialogTitle> 118 | {translate('simple.create-post')} 119 | </DialogTitle> 120 | <DialogContent> 121 | <PostPreview 122 | id={values.post_id} 123 | basePath="/posts" 124 | resource="posts" 125 | /> 126 | </DialogContent> 127 | <DialogActions> 128 | <Button 129 | data-testid="button-close-modal" 130 | onClick={handleCloseShow} 131 | > 132 | {translate('simple.action.close')} 133 | </Button> 134 | </DialogActions> 135 | </Dialog> 136 | </Fragment> 137 | ) : null 138 | } 139 | /> 140 | <Dialog 141 | data-testid="dialog-add-post" 142 | fullWidth 143 | open={showCreateDialog} 144 | onClose={handleCloseCreate} 145 | aria-label={translate('simple.create-post')} 146 | > 147 | <DialogTitle>{translate('simple.create-post')}</DialogTitle> 148 | <DialogContent> 149 | <PostQuickCreate 150 | onCancel={handleCloseCreate} 151 | onSave={handleSave} 152 | basePath="/posts" 153 | resource="posts" 154 | /> 155 | </DialogContent> 156 | </Dialog> 157 | </Fragment> 158 | ); 159 | }; 160 | 161 | export default PostReferenceInput; 162 | -------------------------------------------------------------------------------- /example/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 | -------------------------------------------------------------------------------- /example/src/customRouteLayout.js: -------------------------------------------------------------------------------- 1 | import 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 | <div> 14 | <Title title="Example Admin" /> 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 | -------------------------------------------------------------------------------- /example/src/customRouteNoLayout.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { crudGetList } from 'react-admin'; 4 | 5 | const CustomRouteNoLayout = () => { 6 | const dispatch = useDispatch(); 7 | 8 | const loaded = useSelector( 9 | state => 10 | state.admin.resources.posts && 11 | state.admin.resources.posts.list.total > 0 12 | ); 13 | 14 | const total = useSelector(state => 15 | state.admin.resources.posts ? state.admin.resources.posts.list.total : 0 16 | ); 17 | 18 | useEffect(() => { 19 | dispatch( 20 | crudGetList( 21 | 'posts', 22 | { page: 0, perPage: 10 }, 23 | { field: 'id', order: 'ASC' } 24 | ) 25 | ); 26 | }, [dispatch]); 27 | 28 | return ( 29 | <div> 30 | <h1>Posts</h1> 31 | {!loaded && <p className="app-loader">Loading...</p>} 32 | {loaded && ( 33 | <p> 34 | Found <span className="total">{total}</span> posts ! 35 | </p> 36 | )} 37 | </div> 38 | ); 39 | }; 40 | 41 | export default CustomRouteNoLayout; 42 | -------------------------------------------------------------------------------- /example/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 | -------------------------------------------------------------------------------- /example/src/dataProvider.js: -------------------------------------------------------------------------------- 1 | import fakeRestProvider from 'ra-data-fakerest'; 2 | 3 | import data from './data'; 4 | import addUploadFeature from './addUploadFeature'; 5 | 6 | const dataProvider = fakeRestProvider(data, true); 7 | const uploadCapableDataProvider = addUploadFeature(dataProvider); 8 | const sometimesFailsDataProvider = new Proxy(uploadCapableDataProvider, { 9 | get: (target, name) => (resource, params) => { 10 | // add rejection by type or resource here for tests, e.g. 11 | // if (name === 'delete' && resource === 'posts') { 12 | // return Promise.reject(new Error('deletion error')); 13 | // } 14 | if ( 15 | resource === 'posts' && 16 | params.data && 17 | params.data.title === 'f00bar' 18 | ) { 19 | return Promise.reject(new Error('this title cannot be used')); 20 | } 21 | return uploadCapableDataProvider[name](resource, params); 22 | }, 23 | }); 24 | const delayedDataProvider = new Proxy(sometimesFailsDataProvider, { 25 | get: (target, name) => (resource, params) => 26 | new Promise(resolve => 27 | setTimeout( 28 | () => 29 | resolve(sometimesFailsDataProvider[name](resource, params)), 30 | 300 31 | ) 32 | ), 33 | }); 34 | 35 | export default delayedDataProvider; 36 | -------------------------------------------------------------------------------- /example/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 | -------------------------------------------------------------------------------- /example/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 | -------------------------------------------------------------------------------- /example/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 | -------------------------------------------------------------------------------- /example/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 | -------------------------------------------------------------------------------- /example/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 | -------------------------------------------------------------------------------- /example/src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint react/jsx-key: off */ 2 | import 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 CustomRouteNoLayout from './customRouteNoLayout'; 11 | import dataProvider from './dataProvider'; 12 | import i18nProvider from './i18nProvider'; 13 | import Layout from './Layout'; 14 | import posts from './posts'; 15 | import tags from './tags'; 16 | 17 | render( 18 | } 29 | noLayout 30 | />, 31 | } 35 | />, 36 | ]} 37 | > 38 | {permissions => [ 39 | , 40 | , 41 | permissions ? : null, 42 | , 43 | ]} 44 | , 45 | document.getElementById('root') 46 | ); 47 | -------------------------------------------------------------------------------- /example/src/posts/PostCreate.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useMemo } from 'react'; 2 | import RichTextInput from 'ra-input-rich-text'; 3 | import { 4 | ArrayInput, 5 | AutocompleteInput, 6 | BooleanInput, 7 | Create, 8 | DateInput, 9 | FormDataConsumer, 10 | NumberInput, 11 | ReferenceInput, 12 | SaveButton, 13 | SelectInput, 14 | SimpleForm, 15 | SimpleFormIterator, 16 | TextInput, 17 | Toolbar, 18 | required, 19 | useCreate, 20 | useRedirect, 21 | useNotify, 22 | } from 'react-admin'; // eslint-disable-line import/no-unresolved 23 | import { useFormState, FormSpy } from 'react-final-form'; 24 | 25 | const SaveWithNoteButton = props => { 26 | const [create] = useCreate('posts'); 27 | const redirectTo = useRedirect(); 28 | const notify = useNotify(); 29 | const { basePath, redirect } = props; 30 | 31 | const formState = useFormState(); 32 | const handleClick = useCallback(() => { 33 | if (!formState.valid) { 34 | return; 35 | } 36 | 37 | create( 38 | { 39 | payload: { 40 | data: { ...formState.values, average_note: 10 }, 41 | }, 42 | }, 43 | { 44 | onSuccess: ({ data: newRecord }) => { 45 | notify('ra.notification.created', 'info', { 46 | smart_count: 1, 47 | }); 48 | redirectTo(redirect, basePath, newRecord.id, newRecord); 49 | }, 50 | } 51 | ); 52 | }, [ 53 | formState.valid, 54 | formState.values, 55 | create, 56 | notify, 57 | redirectTo, 58 | redirect, 59 | basePath, 60 | ]); 61 | 62 | return ; 63 | }; 64 | 65 | const PostCreateToolbar = props => ( 66 | 67 | 72 | 78 | 84 | 90 | 91 | ); 92 | 93 | const backlinksDefaultValue = [ 94 | { 95 | date: new Date(), 96 | url: 'http://google.com', 97 | }, 98 | ]; 99 | const PostCreate = ({ permissions, ...props }) => { 100 | const initialValues = useMemo( 101 | () => ({ 102 | average_note: 0, 103 | }), 104 | [] 105 | ); 106 | 107 | const dateDefaultValue = useMemo(() => new Date(), []); 108 | 109 | return ( 110 | 111 | } 113 | initialValues={initialValues} 114 | validate={values => { 115 | const errors = {}; 116 | ['title', 'teaser'].forEach(field => { 117 | if (!values[field]) { 118 | errors[field] = 'Required field'; 119 | } 120 | }); 121 | 122 | if (values.average_note < 0 || values.average_note > 5) { 123 | errors.average_note = 'Should be between 0 and 5'; 124 | } 125 | 126 | return errors; 127 | }} 128 | > 129 | 130 | 131 | 132 | 133 | {({ values }) => 134 | values.title ? ( 135 | 136 | ) : null 137 | } 138 | 139 | 140 | 144 | 145 | 150 | 151 | 152 | 153 | 154 | 155 | {permissions === 'admin' && ( 156 | 157 | 158 | 163 | 164 | 165 | 166 | {({ 167 | formData, 168 | scopedFormData, 169 | getSource, 170 | ...rest 171 | }) => 172 | scopedFormData && scopedFormData.user_id ? ( 173 | 192 | ) : null 193 | } 194 | 195 | 196 | 197 | )} 198 | 199 | 200 | ); 201 | }; 202 | 203 | export default PostCreate; 204 | -------------------------------------------------------------------------------- /example/src/posts/PostDeleteConfirm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | ChipField, 4 | DateField, 5 | ReferenceArrayField, 6 | SimpleShowLayout, 7 | SingleFieldList, 8 | TextField, 9 | } from 'react-admin'; 10 | 11 | // Define your custom title of confirm dialog 12 | const DeleteConfirmTitle = "Are you sure you want to delete this post?"; 13 | 14 | // Define your custom contents of confirm dialog 15 | const DeleteConfirmContent = props => { 16 | return ( 17 | 18 | 19 | 20 | 23 | 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | }; 36 | 37 | export { DeleteConfirmTitle, DeleteConfirmContent }; 38 | -------------------------------------------------------------------------------- /example/src/posts/PostEdit.js: -------------------------------------------------------------------------------- 1 | import RichTextInput from 'ra-input-rich-text'; 2 | import React from 'react'; 3 | import { 4 | TopToolbar, 5 | AutocompleteArrayInput, 6 | AutocompleteInput, 7 | ArrayInput, 8 | BooleanInput, 9 | CheckboxGroupInput, 10 | Datagrid, 11 | DateField, 12 | DateInput, 13 | Edit, 14 | CloneButton, 15 | ShowButton, 16 | EditButton, 17 | FormTab, 18 | ImageField, 19 | ImageInput, 20 | NumberInput, 21 | ReferenceArrayInput, 22 | ReferenceManyField, 23 | ReferenceInput, 24 | SaveButton, 25 | SelectInput, 26 | SimpleFormIterator, 27 | TabbedForm, 28 | TextField, 29 | TextInput, 30 | Toolbar, 31 | minValue, 32 | number, 33 | required, 34 | FormDataConsumer, 35 | } from 'react-admin'; // eslint-disable-line import/no-unresolved 36 | import PostTitle from './PostTitle'; 37 | import { makeStyles } from '@material-ui/core/styles'; 38 | 39 | import DeleteWithCustomConfirmButton from "ra-delete-with-custom-confirm-button"; 40 | import { 41 | DeleteConfirmTitle, 42 | DeleteConfirmContent 43 | } from './PostDeleteConfirm'; 44 | 45 | const useToolbarStyles = makeStyles({ 46 | toolbar: { 47 | display: 'flex', 48 | justifyContent: 'space-between', 49 | }, 50 | }); 51 | 52 | const EditActions = ({ basePath, data, hasShow }) => ( 53 | 54 | 59 | {hasShow && } 60 | 61 | ); 62 | 63 | const CustomToolbar = props => { 64 | const classes = useToolbarStyles(); 65 | return ( 66 | 67 | 68 | 72 | 73 | ); 74 | }; 75 | 76 | 77 | const PostEdit = ({ permissions, ...props }) => ( 78 | } actions={} {...props}> 79 | } 82 | > 83 | 84 | 85 | 86 | 93 | 101 | 102 | 103 | 104 | {permissions === 'admin' && ( 105 | 106 | 107 | 112 | 113 | 114 | 115 | {({ 116 | formData, 117 | scopedFormData, 118 | getSource, 119 | ...rest 120 | }) => 121 | scopedFormData && scopedFormData.user_id ? ( 122 | 141 | ) : null 142 | } 143 | 144 | 145 | 146 | )} 147 | 148 | 149 | 155 | 156 | 157 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 180 | 184 | 185 | 186 | 187 | 188 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | ); 205 | 206 | export default PostEdit; 207 | -------------------------------------------------------------------------------- /example/src/posts/PostList.js: -------------------------------------------------------------------------------- 1 | import BookIcon from '@material-ui/icons/Book'; 2 | import Chip from '@material-ui/core/Chip'; 3 | import { useMediaQuery, makeStyles } from '@material-ui/core'; 4 | import React, { Children, Fragment, cloneElement } from 'react'; 5 | import lodashGet from 'lodash/get'; 6 | import jsonExport from 'jsonexport/dist'; 7 | import { 8 | BooleanField, 9 | BulkDeleteButton, 10 | BulkExportButton, 11 | ChipField, 12 | Datagrid, 13 | DateField, 14 | downloadCSV, 15 | EditButton, 16 | Filter, 17 | List, 18 | NumberField, 19 | ReferenceArrayField, 20 | SearchInput, 21 | ShowButton, 22 | SimpleList, 23 | SingleFieldList, 24 | TextField, 25 | TextInput, 26 | useTranslate, 27 | } from 'react-admin'; // eslint-disable-line import/no-unresolved 28 | 29 | import ResetViewsButton from './ResetViewsButton'; 30 | export const PostIcon = BookIcon; 31 | 32 | import DeleteWithCustomConfirmButton from "ra-delete-with-custom-confirm-button"; 33 | import { 34 | DeleteConfirmTitle, 35 | DeleteConfirmContent 36 | } from './PostDeleteConfirm'; 37 | 38 | const useQuickFilterStyles = makeStyles(theme => ({ 39 | chip: { 40 | marginBottom: theme.spacing(1), 41 | }, 42 | })); 43 | const QuickFilter = ({ label }) => { 44 | const translate = useTranslate(); 45 | const classes = useQuickFilterStyles(); 46 | return ; 47 | }; 48 | 49 | const PostFilter = props => ( 50 | 51 | 52 | 56 | 61 | 62 | ); 63 | 64 | const exporter = posts => { 65 | const data = posts.map(post => ({ 66 | ...post, 67 | backlinks: lodashGet(post, 'backlinks', []).map( 68 | backlink => backlink.url 69 | ), 70 | })); 71 | jsonExport(data, (err, csv) => downloadCSV(csv, 'posts')); 72 | }; 73 | 74 | const useStyles = makeStyles(theme => ({ 75 | title: { 76 | maxWidth: '20em', 77 | overflow: 'hidden', 78 | textOverflow: 'ellipsis', 79 | whiteSpace: 'nowrap', 80 | }, 81 | hiddenOnSmallScreens: { 82 | [theme.breakpoints.down('md')]: { 83 | display: 'none', 84 | }, 85 | }, 86 | publishedAt: { fontStyle: 'italic' }, 87 | })); 88 | 89 | const PostListBulkActions = props => ( 90 | 91 | 92 | 93 | 94 | 95 | ); 96 | 97 | const usePostListActionToolbarStyles = makeStyles({ 98 | toolbar: { 99 | alignItems: 'center', 100 | display: 'flex', 101 | marginTop: -1, 102 | marginBottom: -1, 103 | }, 104 | }); 105 | 106 | const PostListActionToolbar = ({ children, ...props }) => { 107 | const classes = usePostListActionToolbarStyles(); 108 | return ( 109 |
110 | {Children.map(children, button => cloneElement(button, props))} 111 |
112 | ); 113 | }; 114 | 115 | const rowClick = (id, basePath, record) => { 116 | if (record.commentable) { 117 | return 'edit'; 118 | } 119 | 120 | return 'show'; 121 | }; 122 | 123 | const PostPanel = ({ id, record, resource }) => ( 124 |
125 | ); 126 | 127 | const PostList = props => { 128 | const classes = useStyles(); 129 | const isSmall = useMediaQuery(theme => theme.breakpoints.down('sm')); 130 | return ( 131 | } 134 | filters={} 135 | sort={{ field: 'published_at', order: 'DESC' }} 136 | exporter={exporter} 137 | > 138 | {isSmall ? ( 139 | record.title} 141 | secondaryText={record => `${record.views} views`} 142 | tertiaryText={record => 143 | new Date(record.published_at).toLocaleDateString() 144 | } 145 | /> 146 | ) : ( 147 | 148 | 149 | 150 | 154 | 155 | 160 | 161 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 180 | 181 | 182 | )} 183 | 184 | ); 185 | }; 186 | 187 | export default PostList; 188 | -------------------------------------------------------------------------------- /example/src/posts/PostShow.js: -------------------------------------------------------------------------------- 1 | import { useShowController } from 'ra-core'; 2 | import 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 | -------------------------------------------------------------------------------- /example/src/posts/PostTitle.js: -------------------------------------------------------------------------------- 1 | import 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 | -------------------------------------------------------------------------------- /example/src/posts/ResetViewsButton.js: -------------------------------------------------------------------------------- 1 | import 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 | -------------------------------------------------------------------------------- /example/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 | -------------------------------------------------------------------------------- /example/src/tags/TagCreate.js: -------------------------------------------------------------------------------- 1 | /* eslint react/jsx-key: off */ 2 | import 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 | -------------------------------------------------------------------------------- /example/src/tags/TagDeleteConfirm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | SimpleShowLayout, 4 | TextField, 5 | } from 'react-admin'; 6 | 7 | // Define your custom title of confirm dialog 8 | const DeleteConfirmTitle = "Are you sure you want to delete this tag?"; 9 | 10 | // Define your custom contents of confirm dialog 11 | const DeleteConfirmContent = props => { 12 | return ( 13 | 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | export { DeleteConfirmTitle, DeleteConfirmContent }; 21 | -------------------------------------------------------------------------------- /example/src/tags/TagEdit.js: -------------------------------------------------------------------------------- 1 | /* eslint react/jsx-key: off */ 2 | import React from 'react'; 3 | import { 4 | Edit, 5 | SaveButton, 6 | SimpleForm, 7 | TextField, 8 | TextInput, 9 | Toolbar, 10 | required 11 | } from 'react-admin'; 12 | import { makeStyles } from '@material-ui/core/styles'; 13 | 14 | import DeleteWithCustomConfirmButton from "ra-delete-with-custom-confirm-button"; 15 | import { 16 | DeleteConfirmTitle, 17 | DeleteConfirmContent 18 | } from './TagDeleteConfirm'; 19 | 20 | const useToolbarStyles = makeStyles({ 21 | toolbar: { 22 | display: 'flex', 23 | justifyContent: 'space-between', 24 | }, 25 | }); 26 | 27 | const CustomToolbar = props => { 28 | const classes = useToolbarStyles(); 29 | return ( 30 | 31 | 32 | 36 | 37 | ); 38 | }; 39 | 40 | const TagEdit = props => ( 41 | 42 | } 45 | > 46 | 47 | 48 | 49 | 50 | ); 51 | 52 | export default TagEdit; 53 | -------------------------------------------------------------------------------- /example/src/tags/TagList.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useState } from 'react'; 2 | import { List, EditButton } from 'react-admin'; 3 | import { 4 | List as MuiList, 5 | ListItem, 6 | ListItemText, 7 | ListItemSecondaryAction, 8 | Collapse, 9 | Card, 10 | makeStyles, 11 | } from '@material-ui/core'; 12 | import ExpandLess from '@material-ui/icons/ExpandLess'; 13 | import ExpandMore from '@material-ui/icons/ExpandMore'; 14 | 15 | const useStyles = makeStyles({ 16 | card: { 17 | maxWidth: '20em', 18 | marginTop: '1em', 19 | }, 20 | }); 21 | const SmallCard = ({ className, ...props }) => { 22 | const classes = useStyles(); 23 | return ; 24 | }; 25 | 26 | const SubTree = ({ level, root, getChildNodes, openChildren, toggleNode }) => { 27 | const childNodes = getChildNodes(root); 28 | const hasChildren = childNodes.length > 0; 29 | const open = openChildren.includes(root.id); 30 | return ( 31 | 32 | hasChildren && toggleNode(root)} 35 | style={{ paddingLeft: level * 16 }} 36 | > 37 | {hasChildren && open && } 38 | {hasChildren && !open && } 39 | {!hasChildren &&
 
} 40 | 41 | 42 | 43 | 44 | 45 |
46 | 47 | 48 | {childNodes.map(node => ( 49 | 57 | ))} 58 | 59 | 60 |
61 | ); 62 | }; 63 | 64 | const Tree = ({ ids, data }) => { 65 | const [openChildren, setOpenChildren] = useState([]); 66 | const toggleNode = node => 67 | setOpenChildren(state => { 68 | if (state.includes(node.id)) { 69 | return [ 70 | ...state.splice(0, state.indexOf(node.id)), 71 | ...state.splice(state.indexOf(node.id) + 1, state.length), 72 | ]; 73 | } else { 74 | return [...state, node.id]; 75 | } 76 | }); 77 | const nodes = ids.map(id => data[id]); 78 | const roots = nodes.filter(node => typeof node.parent_id === 'undefined'); 79 | const getChildNodes = root => 80 | nodes.filter(node => node.parent_id === root.id); 81 | return ( 82 | 83 | {roots.map(root => ( 84 | 92 | ))} 93 | 94 | ); 95 | }; 96 | 97 | const TagList = props => ( 98 | 105 | 106 | 107 | ); 108 | 109 | export default TagList; 110 | -------------------------------------------------------------------------------- /example/src/tags/TagShow.js: -------------------------------------------------------------------------------- 1 | import 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 | -------------------------------------------------------------------------------- /example/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 | -------------------------------------------------------------------------------- /example/src/validators.js: -------------------------------------------------------------------------------- 1 | import { 2 | required as createRequiredValidator, 3 | number as createNumberValidator, 4 | } from 'react-admin'; 5 | 6 | export const required = createRequiredValidator(); 7 | export const number = createNumberValidator(); 8 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 2 | const HardSourceWebpackPlugin = require('hard-source-webpack-plugin'); 3 | const IgnoreNotFoundExportPlugin = require('ignore-not-found-export-plugin'); 4 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer') 5 | .BundleAnalyzerPlugin; 6 | 7 | module.exports = { 8 | devtool: 'cheap-module-source-map', 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.(t|j)sx?$/, 13 | exclude: /node_modules/, 14 | use: { loader: 'babel-loader' }, 15 | }, 16 | { 17 | test: /\.html$/, 18 | exclude: /node_modules/, 19 | use: { loader: 'html-loader' }, 20 | }, 21 | ], 22 | }, 23 | plugins: [ 24 | new HtmlWebpackPlugin({ 25 | template: './src/index.html', 26 | }), 27 | new HardSourceWebpackPlugin(), 28 | // required because of https://github.com/babel/babel/issues/7640 29 | new IgnoreNotFoundExportPlugin([ 30 | 'CallbackSideEffect', 31 | 'ChoicesProps', 32 | 'InputProps', 33 | 'NotificationSideEffect', 34 | 'OptionText', 35 | 'OptionTextElement', 36 | 'RedirectionSideEffect', 37 | 'RefreshSideEffect', 38 | 'AdminUIProps', 39 | 'AdminContextProps', 40 | 'AdminRouterProps', 41 | ]), 42 | ].concat( 43 | process.env.NODE_ENV === 'development' 44 | ? [new BundleAnalyzerPlugin()] 45 | : [] 46 | ), 47 | resolve: { 48 | extensions: ['.ts', '.js', '.tsx', '.json'], 49 | }, 50 | devServer: { 51 | stats: { 52 | children: false, 53 | chunks: false, 54 | modules: false, 55 | }, 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /img/ra-delete-with-custom-confirm-button.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itTkm/ra-delete-with-custom-confirm-button/c61c2bc579800b2d6a8e4ddd24f36ad091466215/img/ra-delete-with-custom-confirm-button.gif -------------------------------------------------------------------------------- /lib/DeleteWithCustomConfirmButton.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = void 0; 9 | 10 | var _react = _interopRequireWildcard(require("react")); 11 | 12 | var _reactRedux = require("react-redux"); 13 | 14 | var _reactAdmin = require("react-admin"); 15 | 16 | var _raCore = require("ra-core"); 17 | 18 | var _raCustomConfirm = _interopRequireDefault(require("ra-custom-confirm")); 19 | 20 | var _classnames = _interopRequireDefault(require("classnames")); 21 | 22 | var _propTypes = _interopRequireDefault(require("prop-types")); 23 | 24 | var _compose = _interopRequireDefault(require("recompose/compose")); 25 | 26 | var _styles = require("@material-ui/core/styles"); 27 | 28 | var _colorManipulator = require("@material-ui/core/styles/colorManipulator"); 29 | 30 | var _Delete = _interopRequireDefault(require("@material-ui/icons/Delete")); 31 | 32 | var _ErrorOutline = _interopRequireDefault(require("@material-ui/icons/ErrorOutline")); 33 | 34 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 35 | 36 | function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; } 37 | 38 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } 39 | 40 | function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } 41 | 42 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 43 | 44 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 45 | 46 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 47 | 48 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } 49 | 50 | function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } 51 | 52 | function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; } 53 | 54 | function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } 55 | 56 | function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } 57 | 58 | function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } } 59 | 60 | function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } 61 | 62 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 63 | 64 | var styles = function styles(theme) { 65 | return { 66 | deleteButton: { 67 | color: theme.palette.error.main, 68 | '&:hover': { 69 | backgroundColor: (0, _colorManipulator.fade)(theme.palette.error.main, 0.12), 70 | // Reset on mouse devices 71 | '@media (hover: none)': { 72 | backgroundColor: 'transparent' 73 | } 74 | } 75 | } 76 | }; 77 | }; 78 | 79 | var DeleteWithCustomConfirmButton = /*#__PURE__*/function (_Component) { 80 | _inherits(DeleteWithCustomConfirmButton, _Component); 81 | 82 | var _super = _createSuper(DeleteWithCustomConfirmButton); 83 | 84 | function DeleteWithCustomConfirmButton() { 85 | var _this; 86 | 87 | _classCallCheck(this, DeleteWithCustomConfirmButton); 88 | 89 | for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { 90 | args[_key] = arguments[_key]; 91 | } 92 | 93 | _this = _super.call.apply(_super, [this].concat(args)); 94 | 95 | _defineProperty(_assertThisInitialized(_this), "state", { 96 | showDialog: false 97 | }); 98 | 99 | _defineProperty(_assertThisInitialized(_this), "handleClick", function (event) { 100 | event.stopPropagation(); 101 | 102 | _this.setState({ 103 | showDialog: true 104 | }); 105 | }); 106 | 107 | _defineProperty(_assertThisInitialized(_this), "handleDialogClose", function () { 108 | _this.setState({ 109 | showDialog: false 110 | }); 111 | }); 112 | 113 | _defineProperty(_assertThisInitialized(_this), "handleDelete", function (event) { 114 | event.stopPropagation(); 115 | event.preventDefault(); 116 | 117 | _this.setState({ 118 | showDialog: false 119 | }); 120 | 121 | var _this$props = _this.props, 122 | dispatchCrudDelete = _this$props.dispatchCrudDelete, 123 | startUndoable = _this$props.startUndoable, 124 | resource = _this$props.resource, 125 | record = _this$props.record, 126 | basePath = _this$props.basePath, 127 | redirect = _this$props.redirect, 128 | undoable = _this$props.undoable; 129 | 130 | if (undoable) { 131 | startUndoable((0, _raCore.crudDelete)(resource, record.id, record, basePath, redirect)); 132 | } else { 133 | dispatchCrudDelete(resource, record.id, record, basePath, redirect); 134 | } 135 | }); 136 | 137 | return _this; 138 | } 139 | 140 | _createClass(DeleteWithCustomConfirmButton, [{ 141 | key: "render", 142 | value: function render() { 143 | var showDialog = this.state.showDialog; 144 | var _this$props2 = this.props, 145 | cancel = _this$props2.cancel, 146 | CancelIcon = _this$props2.CancelIcon, 147 | classes = _this$props2.classes, 148 | className = _this$props2.className, 149 | content = _this$props2.content, 150 | confirmColor = _this$props2.confirmColor, 151 | DeleteIcon = _this$props2.DeleteIcon, 152 | label = _this$props2.label, 153 | title = _this$props2.title; 154 | return _react["default"].createElement(_react.Fragment, null, _react["default"].createElement(_reactAdmin.Button, { 155 | label: label, 156 | onClick: this.handleClick, 157 | className: (0, _classnames["default"])('ra-delete-button', classes.deleteButton, className) 158 | }, _react["default"].createElement(DeleteIcon, null)), _react["default"].createElement(_raCustomConfirm["default"], _extends({}, this.props, { 159 | isOpen: showDialog, 160 | title: title // your custom title of confirm dialog 161 | , 162 | content: content // your custom contents of confirm dialog 163 | , 164 | confirm: label // label of confirm button (default: 'Confirm') 165 | , 166 | confirmColor: confirmColor // color of confirm button ('primary' or 'warning', default: 'primary') 167 | , 168 | ConfirmIcon: DeleteIcon // icon of confirm button (default: 'ActionCheck') 169 | , 170 | cancel: cancel // label of cancel button (default: 'Cancel') 171 | , 172 | CancelIcon: CancelIcon // icon of cancel button (default: 'AlertError') 173 | , 174 | onConfirm: this.handleDelete, 175 | onClose: this.handleDialogClose 176 | }))); 177 | } 178 | }]); 179 | 180 | return DeleteWithCustomConfirmButton; 181 | }(_react.Component); 182 | 183 | DeleteWithCustomConfirmButton.propTypes = { 184 | basePath: _propTypes["default"].string, 185 | classes: _propTypes["default"].object, 186 | className: _propTypes["default"].string, 187 | confirmColor: _propTypes["default"].string.isRequired, 188 | cancel: _propTypes["default"].string, 189 | CancelIcon: _propTypes["default"].elementType, 190 | content: _propTypes["default"].element.isRequired, 191 | dispatchCrudDelete: _propTypes["default"].func.isRequired, 192 | DeleteIcon: _propTypes["default"].elementType, 193 | label: _propTypes["default"].string, 194 | record: _propTypes["default"].object, 195 | redirect: _propTypes["default"].oneOfType([_propTypes["default"].string, _propTypes["default"].bool, _propTypes["default"].func]), 196 | resource: _propTypes["default"].string.isRequired, 197 | startUndoable: _propTypes["default"].func, 198 | title: _propTypes["default"].string.isRequired, 199 | translate: _propTypes["default"].func, 200 | undoable: _propTypes["default"].bool 201 | }; 202 | DeleteWithCustomConfirmButton.defaultProps = { 203 | cancel: 'ra.action.cancel', 204 | CancelIcon: _ErrorOutline["default"], 205 | confirmColor: 'warning', 206 | DeleteIcon: _Delete["default"], 207 | label: 'ra.action.delete', 208 | redirect: 'list', 209 | startUndoable: _raCore.startUndoable, 210 | undoable: true 211 | }; 212 | 213 | var _default = (0, _compose["default"])((0, _reactRedux.connect)(null, { 214 | startUndoable: _raCore.startUndoable, 215 | dispatchCrudDelete: _raCore.crudDelete 216 | }), _raCore.translate, (0, _styles.withStyles)(styles))(DeleteWithCustomConfirmButton); 217 | 218 | exports["default"] = _default; -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports["default"] = void 0; 7 | 8 | var _DeleteWithCustomConfirmButton = _interopRequireDefault(require("./DeleteWithCustomConfirmButton")); 9 | 10 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 11 | 12 | var _default = _DeleteWithCustomConfirmButton["default"]; 13 | exports["default"] = _default; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ra-delete-with-custom-confirm-button", 3 | "version": "2.0.2", 4 | "description": "Delete button with your custom confirm dialog for React-admin.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "echo \"WARNING: no test specified\" && exit 0", 8 | "lint": "eslint src", 9 | "build": "babel src --out-dir lib", 10 | "preversion": "npm test", 11 | "version": "npm run build && git add --all lib", 12 | "postversion": "git push && git push --tags && npm publish" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/itTkm/ra-delete-with-custom-confirm-button.git" 17 | }, 18 | "keywords": [ 19 | "react", 20 | "react-admin", 21 | "delete", 22 | "button", 23 | "custom", 24 | "confirm" 25 | ], 26 | "author": "itTkm", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/itTkm/ra-delete-with-custom-confirm-button/issues" 30 | }, 31 | "homepage": "https://github.com/itTkm/ra-delete-with-custom-confirm-button#readme", 32 | "dependencies": { 33 | "@material-ui/core": "^4.9.4", 34 | "classnames": "^2.2.6", 35 | "prop-types": "^15.7.2", 36 | "ra-custom-confirm": "^1.1.1", 37 | "ra-ui-materialui": "^3.13.1", 38 | "recompose": "^0.30.0" 39 | }, 40 | "devDependencies": { 41 | "@babel/cli": "^7.13.0", 42 | "@babel/core": "^7.9.0", 43 | "@babel/plugin-proposal-class-properties": "^7.8.3", 44 | "@babel/preset-env": "^7.9.0", 45 | "@babel/preset-react": "^7.8.3", 46 | "@material-ui/icons": "^4.9.1", 47 | "babel-eslint": "^10.1.0", 48 | "babel-loader": "^8.0.6", 49 | "eslint": "^6.8.0", 50 | "eslint-plugin-import": "^2.20.1", 51 | "eslint-plugin-react": "^7.18.3", 52 | "ra-core": "^3.13.1", 53 | "react": "^16.13.0", 54 | "react-redux": "^7.2.0" 55 | }, 56 | "peerDependencies": { 57 | "@material-ui/core": ">=1.4.0", 58 | "@material-ui/icons": ">=1.0.0", 59 | "ra-core": ">=2.3.4", 60 | "ra-ui-materialui": ">=2.3.4", 61 | "react": ">=16.3.0", 62 | "react-redux": ">=5.0.7" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/DeleteWithCustomConfirmButton.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Button } from 'react-admin'; 4 | import { 5 | translate, 6 | crudDelete, 7 | startUndoable, 8 | } from 'ra-core'; 9 | import CustomConfirm from 'ra-custom-confirm'; 10 | import classnames from 'classnames'; 11 | import PropTypes from 'prop-types'; 12 | import compose from 'recompose/compose'; 13 | import { withStyles } from '@material-ui/core/styles'; 14 | import { fade } from '@material-ui/core/styles/colorManipulator'; 15 | import ActionDelete from '@material-ui/icons/Delete'; 16 | import AlertError from '@material-ui/icons/ErrorOutline'; 17 | 18 | const styles = (theme) => ({ 19 | deleteButton: { 20 | color: theme.palette.error.main, 21 | '&:hover': { 22 | backgroundColor: fade(theme.palette.error.main, 0.12), 23 | // Reset on mouse devices 24 | '@media (hover: none)': { 25 | backgroundColor: 'transparent' 26 | } 27 | } 28 | } 29 | }); 30 | 31 | class DeleteWithCustomConfirmButton extends Component { 32 | state = { 33 | showDialog: false 34 | }; 35 | 36 | handleClick = (event) => { 37 | event.stopPropagation(); 38 | this.setState({ showDialog: true }); 39 | }; 40 | 41 | handleDialogClose = () => { 42 | this.setState({ showDialog: false }); 43 | }; 44 | 45 | handleDelete = (event) => { 46 | event.stopPropagation(); 47 | event.preventDefault(); 48 | this.setState({ showDialog: false }); 49 | const { 50 | dispatchCrudDelete, 51 | startUndoable, 52 | resource, 53 | record, 54 | basePath, 55 | redirect, 56 | undoable, 57 | } = this.props; 58 | if (undoable) { 59 | startUndoable(crudDelete(resource, record.id, record, basePath, redirect)); 60 | } else { 61 | dispatchCrudDelete(resource, record.id, record, basePath, redirect); 62 | } 63 | }; 64 | 65 | render() { 66 | const { showDialog } = this.state; 67 | const { 68 | cancel, 69 | CancelIcon, 70 | classes, 71 | className, 72 | content, 73 | confirmColor, 74 | DeleteIcon, 75 | label, 76 | title, 77 | } = this.props; 78 | 79 | return ( 80 | 81 | 88 | 100 | 101 | ); 102 | } 103 | } 104 | 105 | DeleteWithCustomConfirmButton.propTypes = { 106 | basePath: PropTypes.string, 107 | classes: PropTypes.object, 108 | className: PropTypes.string, 109 | confirmColor: PropTypes.string.isRequired, 110 | cancel: PropTypes.string, 111 | CancelIcon: PropTypes.elementType, 112 | content: PropTypes.element.isRequired, 113 | dispatchCrudDelete: PropTypes.func.isRequired, 114 | DeleteIcon: PropTypes.elementType, 115 | label: PropTypes.string, 116 | record: PropTypes.object, 117 | redirect: PropTypes.oneOfType([PropTypes.string, PropTypes.bool, PropTypes.func]), 118 | resource: PropTypes.string.isRequired, 119 | startUndoable: PropTypes.func, 120 | title: PropTypes.string.isRequired, 121 | translate: PropTypes.func, 122 | undoable: PropTypes.bool 123 | }; 124 | 125 | DeleteWithCustomConfirmButton.defaultProps = { 126 | cancel: 'ra.action.cancel', 127 | CancelIcon: AlertError, 128 | confirmColor: 'warning', 129 | DeleteIcon: ActionDelete, 130 | label: 'ra.action.delete', 131 | redirect: 'list', 132 | startUndoable: startUndoable, 133 | undoable: true, 134 | }; 135 | 136 | export default compose( 137 | connect( 138 | null, 139 | { startUndoable, dispatchCrudDelete: crudDelete } 140 | ), 141 | translate, 142 | withStyles(styles) 143 | )(DeleteWithCustomConfirmButton); 144 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import DeleteWithCustomConfirmButton from './DeleteWithCustomConfirmButton'; 2 | 3 | export default DeleteWithCustomConfirmButton; 4 | --------------------------------------------------------------------------------