Required props for List and Edit components in route when using CustomApp - react-admin

I'm trying to upgrade a custom app from admin-on-rest to react-admin (v2.15). After I figured out that the declareResources action was "replaced" by registerResource most seemed ok, but I still struggle with the List and Edit components route definitions that complains about missing required props (compared to what props are defined in the custom app documentation).
If I define a List component like this it works fine:
<Route exact path="/mystuffs" render={(routeProps) => <MystuffList hasCreate hasEdit hasShow={false} hasList resource="mystuffs" basePath="/mystuffs" {...routeProps} />} />
Similar the only way I can get an Edit-component to work is to pass the required props like so:
<Route exact path="/mystuffs/:id" render={(routeProps) => <MystuffEdit resource="mystuffs" id={routeProps.match.params.id} basePath="/mystuffs" {...routeProps} />} />
But to me it seems a bit tedious to define all of these props (i.e was not required with admin-on-rest). Is this the correct way of doing it or am I missing something obvious here since the custom app documentation doesn't specify all of the required props?

I ended up adding a custom resource component that is pretty similar to the Resource in react-admin. Something like this:
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { Switch, Route } from 'react-router-dom';
const RACustomResource = ({ resource, list, edit, create, show, path, children, ...rest }) => {
const { computedMatch, ...options } = rest;
const listResource = list ?
(<Route
exact
path={path}
render={(routeProps) => {
const { staticContext, ...routeOpts } = routeProps;
return React.createElement(list, {
basePath: path || routeProps.match.url,
resource,
hasCreate: !!create,
hasList: !!list,
hasEdit: !!edit,
hasShow: !!show,
...routeOpts,
...options,
});
}}
/>)
: null;
const createResource = create ?
(<Route
path={`${path}/create`}
render={(routeProps) => {
const { staticContext, ...routeOpts } = routeProps;
return React.createElement(create, {
basePath: path || routeProps.match.url,
resource,
hasList: !!list,
hasShow: !!show,
record: {},
...routeOpts,
...options,
});
}}
/>)
: null;
const editResource = edit ?
(<Route
exact
path={`${path}/:id`}
render={(routeProps) => {
const { staticContext, ...routeOpts } = routeProps;
return React.createElement(edit, {
basePath: path || routeProps.match.url,
resource,
hasCreate: !!create,
hasList: !!list,
hasEdit: !!edit,
hasShow: !!show,
id: routeProps.match.params.id,
...routeOpts,
...options,
});
}}
/>)
: null;
const showResource = show ?
(<Route
exact
path={`${path}/:id/show`}
render={(routeProps) => {
const { staticContext, ...routeOpts } = routeProps;
return React.createElement(show, {
basePath: path || routeProps.match.url,
resource,
hasCreate: !!create,
hasList: !!list,
hasEdit: !!edit,
hasShow: !!show,
id: routeProps.match.params.id,
...routeOpts,
...options,
});
}}
/>)
: null;
return (
<Switch>
{createResource}
{showResource}
{editResource}
{listResource}
{children}
</Switch>
);
};
RACustomResource.propTypes = {
resource: PropTypes.string.isRequired,
path: PropTypes.string,
basePath: PropTypes.string,
children: PropTypes.any,
list: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
create: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
edit: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
show: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
};
export default RACustomResource;
// used like this
// <RACustomResource path="/myresource" resource="myresource" list={MyResourceList} create={MyResourceCreate} edit={MyResourceEdit} />

It is indeed required. We have still a lot of work to do on the custom app side, including documentation.
You can help us! Can you explain why you needed to use react-admin this way? What wasn't possible using the default Admin? etc.
Thanks!

Related

From react-admin v3 to v4

I am new to react-admin, and i'd like to implement custom forms for related records.
I found this on the doc but the code doesn't seem to work for version 4 anymore.
I'd like to do the same. Open a modal for the related record.
Is there any v4 example ?
React-admin v4 has built-in support for related record creation, see the Creating new choices documentation.
import { SelectInput, Create, SimpleForm, TextInput } from 'react-admin';
const PostCreate = () => {
const categories = [
{ name: 'Tech', id: 'tech' },
{ name: 'Lifestyle', id: 'lifestyle' },
];
return (
<Create>
<SimpleForm>
<TextInput source="title" />
<SelectInput
onCreate={() => {
const newCategoryName = prompt('Enter a new category');
const newCategory = { id: newCategoryName.toLowerCase(), name: newCategoryName };
categories.push(newCategory);
return newCategory;
}}
source="category"
choices={categories}
/>
</SimpleForm>
</Create>
);
}

React admin import button referenceManyField?

I want add an import button for my react admin application.
This import will create records related to another record. For instance, I have a list of contracts that belong to a company.
I saw this repo: https://github.com/benwinding/react-admin-import-csv
And I was wondering if there was something similar for a referenceManyField.
I found a hacky solution for this in case anyone is interested.
Instead of using a ReferenceManyField I used a List inside a Show component.
To be able to do this I had to override the props of the list to specify the resource that I wanted to reference.
I also had to pass the show item id as a prop of the list component and filter the list by the id.
let companyId = '';
const ListActions = (props) => {
const { className } = props;
return (
<TopToolbar className={className}>
<ImportButton {...props} />
</TopToolbar>
);
};
export const ContractRow = ({ contractCompanyId }) => {
companyId = contractCompanyId;
const fakeProps: ListProps = {
basePath: '/contracts',
hasCreate: false,
hasEdit: false,
hasList: true,
hasShow: false,
location: { pathname: '/', search: '', hash: '', state: undefined },
match: { path: '/', url: '/', isExact: true, params: {} },
options: {},
permissions: null,
resource: 'contracts',
};
return (
<List
{...fakeProps}
filter={{ _company_id: contractCompanyId }}
actions={<ListActions />}
>
<Datagrid className={classes.insideTable}>
<TextField source="name" />
</Datagrid>
</List>
);
};

How to pass functions from parent to any children and sub-children using Navigator and WithSecurityScreen in React Native?

I am quite new in React Native and I am trying to understand how function written in a parent Component could be passed (inherited) to any children and sub-children. In particular I am using a library to internationalise my App using:
import * as RNLocalize from 'react-native-localize'
import i18n from 'i18n-js'
But I noticed that I have to implement the translate(...) function for each Component of the whole project and this seems to be exaggerated because it requires a lot of work to implement the translation feature (I followed this tutorial).
Please, note that I have a basic understanding how to pass a function or some data using this.props, so I am not asking how props works from a parent to a single child. What I am asking is: how to avoid to repeat the code from //BEGIN ... to //END... (please see WithSecurityScreen file) and to avoid to repeat the implementation of handleLocalizationChange, RNLocalize.addEventListener, RNLocalize.removeEventListener and translate.
Please also note that the translation library works, test is provided at following line of WithSecurityScreen:
const SecurityScreen = () => <View><Text>{translate('USER_SURNAME')}😂</Text></View>;
But I am not be able to pass translate(...) function to each components of the whole project.
The project structure is:
App.js (wraps SecureApp.js)
SecureApp.js (wrapped in App.js and runs WithSecurityScreen.js)
WithSecurityScreen.js (wraps routes to views, e.g. Welcome.js)
Welcome.js (main view)
App.js
import { withSecurityScreen } from './src/components/withSecurityScreen'
import App from "./SecureApp.js"
export default withSecurityScreen(App);
SecureApp.js
const MainNavigator = createStackNavigator({
Home: {
screen: Welcome,
navigationOptions: {
headerShown: false
}
},
UserProfile: {
screen: CoreApp,
navigationOptions: {
headerShown: false
}
},
NumPad: {
screen: NumPad,
navigationOptions: {
header: 'PIN Creation',
headerShown: false
}
}, /* , navigationOptions: {headerLeft: () => null} */
QrScan: {
screen: QrScan, navigationOptions: {
header: 'QR Scan',
headerShown: false
}
},
...
});
export default createAppContainer(MainNavigator);
WithSecurityScreen.js
// START: https://heartbeat.fritz.ai/how-to-use-react-native-localize-in-react-native-apps-3bb3d510f801
import * as RNLocalize from 'react-native-localize'
import i18n from 'i18n-js'
import memoize from 'lodash.memoize'
const translationGetters = {
en: () => require('./../../assets/locales/en/en.json'),
it: () => require('./../../assets/locales/it/it.json')
};
const translate = memoize(
(key, config) => i18n.t(key, config),
(key, config) => (config ? key + JSON.stringify(config) : key)
)
const setI18nConfig = () => {
const fallback = { languageTag: 'en' }
const { languageTag } =
RNLocalize.findBestAvailableLanguage(Object.keys(translationGetters)) ||
fallback
translate.cache.clear()
i18n.translations = { [languageTag]: translationGetters[languageTag]() }
i18n.locale = languageTag
}
// END: https://heartbeat.fritz.ai/how-to-use-react-native-localize-in-react-native-apps-3bb3d510f801
const SecurityScreen = () => <View><Text>{translate('USER_SURNAME')}😂</Text></View>;
const showSecurityScreenFromAppState = appState =>
['background', 'inactive'].includes(appState);
const withSecurityScreenIOS = Wrapped => {
return class WithSecurityScreen extends React.Component {
constructor(props) {
super(props)
setI18nConfig()
}
state = {
showSecurityScreen: showSecurityScreenFromAppState(AppState.currentState)
};
componentDidMount() {
AppState.addEventListener('change', this.onChangeAppState)
RNLocalize.addEventListener('change', this.handleLocalizationChange)
}
componentWillUnmount() {
AppState.removeEventListener('change', this.onChangeAppState)
RNLocalize.removeEventListener('change', this.handleLocalizationChange)
}
handleLocalizationChange = () => {
setI18nConfig()
.then(() => this.forceUpdate())
.catch(error => {
console.error(error)
})
}
onChangeAppState = nextAppState => {
const showSecurityScreen = showSecurityScreenFromAppState(nextAppState);
this.setState({showSecurityScreen})
};
render() {
return this.state.showSecurityScreen
? <SecurityScreen/>
: <Wrapped {...this.props} />
}
}
};
const withSecurityScreenAndroid = Wrapped => Wrapped;
export const withSecurityScreen = Platform.OS === 'ios'
? withSecurityScreenIOS
: withSecurityScreenAndroid;
Welcome.js
export default class Welcome extends Component {
let username = 'UserName';
render() {
return (
<View style={styles.container}>
<LinearGradient colors={globalStyles.colors.gradientGreen} style={{flex: 1}}>
<View style={styles.upperView}><Text style={styles.upperViewText}>{this.props.translate('WELCOME_TEXT')}{this.username}</Text>
</View>
</LinearGradient>
</View>
);
}
}
I get following error:
First of all in your case you can declare translate function in separate js file locale.js and can declare all your translation logic in that file and export the functions translate and setI18nConfig
local.js
import * as RNLocalize from 'react-native-localize'
import i18n from 'i18n-js'
import memoize from 'lodash.memoize'
const translationGetters = {
en: () => require('./../../assets/locales/en/en.json'),
it: () => require('./../../assets/locales/it/it.json')
};
export const translate = memoize(
(key, config) => i18n.t(key, config),
(key, config) => (config ? key + JSON.stringify(config) : key)
)
export const setI18nConfig = () => {
const fallback = { languageTag: 'en' }
const { languageTag } =
RNLocalize.findBestAvailableLanguage(Object.keys(translationGetters)) ||
fallback
translate.cache.clear()
i18n.translations = { [languageTag]: translationGetters[languageTag]() }
i18n.locale = languageTag
}
and import this functions in your components where you want to use this like
App.js
import React, { Component } from 'react';
import { View, Text } from 'react-native';
import { setI18nConfig, translate } from './locale';
export default class App extends Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
setI18nConfig(); // This should be called only once through out the app at the time of changing locale.
return (
<View>
<Text> translate ('key_from_json_to_label') </Text>
</View>
);
}
}
For more details you can refer this repo where I have implemented the same.
Hope this will help.
i use import i18n from "i18next", for translation.
Here is the config file:
//config/i18n
import i18n from "i18next";
import { reactI18nextModule, initReactI18next } from "react-i18next";
import translationFR from '../translation/fr/translation.json';
import translationEN from '../translation/en/translation.json';
import DeviceInfo from 'react-native-device-info';
let locale = DeviceInfo.getDeviceLocale().substring(0, 2);
if (locale != 'fr') {
locale = 'en';
}
// the translations
const resources = {
en: translationEN,
fr: translationFR,
};
i18n
.use(reactI18nextModule) // passes i18n down to react-i18next
.init({
resources: resources,
lng: locale,
fallbackLng: ['en', 'fr'],
keySeparator: false, // we do not use keys in form messages.welcome
interpolation: {
escapeValue: false // react already safes from xss
}
});
export default i18n;
One of the uses is to
import i18n from 'config/i18n'
and use translate in file like this
i18n.t('bottomTab:home_tab')
You can also wrap component with withNamespaces from 'react-i18next', like this:
export default withNamespaces([], {wait: true})(Welcome)
and then access translation with:
this.props.t('welcome_message')

How can I set a default value on ReferenceManyField component?

I'm rendering a ... which resource is "Users", so always is calling this ReferenceManyField on but sometimes "User" don't have "Name", and I want to display some default if "User" don't have name, I did't find any solution for this problem, sorry. Thank you!
You can replace the contents of the 'record' field before passing it to your components:
<ReferenceManyField reference="Users" target="id" >
<SingleFieldList>
<FormDataConsumer>
{ ({ formData, dispatch, record, ...rest }) => {
const defValues = { Name: 'MyName', Param1: 'Value1', Param2: 'Value2' }
const newRecord = { ...defValues, ...record }
const params = { record: newRecord, ...rest }
return (<TextField source="Name" {...params} />)
}
}
</FormDataConsumer>
</SingleFieldList>
</ReferenceManyField>

Use <List /> on React-Admin dashboard

I'm using react-admin v2.3.2 with a custom dashboard component as shown in the react-admin tutorial.
<Admin dashboard={MyDashboard}>
<Resource name="incidents ... />
</Admin>
Now I'd like to display a list of incidents on my dashboard using the component of react-admin but react-admin complains about missing properties like 'hasEdit'.
I simply passed the props of the dashboard component to the List but this does obviously not work:
class MyDashboard extends React.Component {
constructor(props) {
super(props)
render(
return <List {...this.props}>
<Datagrid> .... </Datagrid>
</List>
)
}
Is it possible to use react-admin's <List /> component on the dashboard and if so how can this be done?
Thanks in advance,
Thomas
I finally managed to use react-admin's components by faking the required props. Within the MyDashboard component I define props required by the component:
let fakeProps = {
basePath: "/incidents",
hasCreate: false,
hasEdit: false,
hasList: true,
hasShow: false,
history: {},
location: { pathname: "/", search: "", hash: "", state: undefined },
match: { path: "/", url: "/", isExact: true, params: {} },
options: {},
permissions: null,
resource: "incidents"
}
<List {...fakeProps}>
<DataGrid>
<TextField .... />
</DataGrid>
</List>
This is indeed a sub-optimal solution but in the first run it solves my problem.
We had a request to create a List in the Dashboard so I had use the accepted answer. Though pagination wouldn't trigger new requests to the server even though the url was changing.
This is the final solution that works with pagination using react-router v4.
In <Admin dashboard={Dashboard}> I added:
<Resource name="dashboard-stats"/>
In the Dashboard.js this is what I have:
import React, { Component } from 'react';
import { GET_LIST } from 'react-admin';
import Card from '#material-ui/core/Card';
import CardHeader from '#material-ui/core/CardHeader';
import dataProvider from './dataProvider';
import {
List,
TextField,
Datagrid
} from 'react-admin';
export default class Dashboard extends Component {
state = {};
initProps = {
basePath: "/",
hasCreate: false,
hasEdit: false,
hasList: true,
hasShow: false,
location: { pathname: "/", search: "", hash: "", state: undefined },
match: { path: "/", url: "/", isExact: true, params: {} },
options: {
},
permissions: null,
resource: "dashboard-stats",
perPage: 5
};
componentWillMount() {
this.unlisten = this.props.history.listen((location, action) => {
if (location.search) {
//regex from: https://stackoverflow.com/a/8649003/1501205
let queryParams = JSON.parse('{"' + decodeURI(location.search).replace(/"/g, '\\"').replace(/&/g, '","').replace(/=/g,'":"') + '"}')
let perPage = queryParams.perPage;
let page = queryParams.page;
this.initProps.perPage = perPage;
this.initProps.page = page;
this.initProps.location = location;
this.setState({'initProps': this.initProps})
}
});
}
componentWillUnmount() {
this.unlisten();
}
componentDidMount() {
this.setState({'initProps': this.initProps});
dataProvider(GET_LIST, 'stats', {
sort: { field: 'date', order: 'DESC' },
pagination: { page: 1, perPage: 50 },
})
.then(response => {
this.setState({'stats': response.data});
});
}
render() {
const {
initProps
} = this.state;
if(!initProps) {
return false;
}
return <Card>
<CardHeader title="Welcome to the Dashboard" />
<List {...initProps} >
<Datagrid>
<TextField source="description" />
<TextField source="state" />
<TextField source="date" />
</Datagrid>
</List>
</Card>;
}
}
don't forget to update the location this.initProps.location with the location just changed - otherwise it will work for the first click (route change) and then it will stop working
I came across this page researching similar.
The way to do this now (late 2020) is to create a ListContextProvider on the page for the main resource you want to show on the dashboard so you get everything provided by that context, including filters.
const controllerProps = useListController(props);
<ListContextProvider value={controllerProps}>
<MyDashboardView>
</ListContextProvider>
You must be wanting to display data from various resources, otherwise you would just use a regular "List" page.
Dashboard can do this. Have a look at the Demo Dashboard
Multiple dataProvider(GET_LIST,... that you pass on to components. You can use this Demo Dashboard Component as an example. Pending Orders
Thanks for the solution. Screen renders with below warning
How to remove all warnings ?
Warning: React does not recognize the basePath prop on a DOM element. If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase basepath instead. If you accidentally passed it from a parent component, remove it from the DOM element.
Like basePath console shows same warnings with other props "currentSort, defaultTitle, displayedFilters,filterValues,hasCreate,hideFilter,isLoading,loadedOnce,onToggleItem,onUnselectItems,perPage,selectedIds,setFilters,setPage,setPerPage,setSort,showFilter,hasBulkActions"
Warning: Invalid value for prop translate on tag. Either remove it from the element, or pass a string or number value to keep it in the DOM. For details
const initProps = {
basePath: '/',
hasCreate: false,
hasEdit: false,
hasList: true,
hasShow: false,
location: { pathname: '/', search: '', hash: '', state: undefined },
match: { path: '/', url: '/', isExact: true, params: {} },
options: {},
permissions: null,
resource: 'dashboard',
perPage: 5
};
<List
{...initProps}
filterDefaultValues={{
arrivedTimestampStart: moment().format('YYYY-MM-DD'),
arrivedTimestamp: moment().format('YYYY-MM-DD')
}}
filters={<DashboardFilter />}
sort={{ field: 'arrivedTimestamp', order: 'DESC' }}
pagination={<Fragment />}
exporter={false}
>
<Responsive
medium={
<div style={styles.flex}>
<OtherComponent />
</div>
}
/>
</List>
I think the answer in v4+ is now the useList hook, which you can see on the List docs page: https://marmelab.com/react-admin/List.html. Copied from that page:
const data = [
{ id: 1, name: 'Arnold' },
{ id: 2, name: 'Sylvester' },
{ id: 3, name: 'Jean-Claude' },
]
const ids = [1, 2, 3];
const MyComponent = () => {
const listContext = useList({
data,
ids,
basePath: '/resource',
resource: 'resource',
});
return (
<ListContextProvider value={listContext}>
<Datagrid>
<TextField source="id" />
<TextField source="name" />
</Datagrid>
</ListContextProvider>
);
};