AOR - UnitTest a simple List - admin-on-rest

I am using AOR v1.4.0 and trying to write a unit test to test the rendering of a simple list with one row. But nothing gets logged on console as HTML
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import * as Renderer from 'react-test-renderer';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import {render, shallow, mount} from 'enzyme';
import {Datagrid, List, Admin, Resource} from 'admin-on-rest';
import {CategoryList} from '../Categories';
describe('Categories', ()=>{
it('renders category list correctly', ()=>{
const wrapper = mount(
<Admin title="Mock Admin Client" restClient={ jest.fn().mockImplementation(()=>{
return new Promise((res, rej)=>{
res({
total : 1,
data: ()=>{
return {
id: "0300b4cf-4888-4e73-b4e1-25cf4686e05c",
name: "cat2",
displaySequence: 121
}
}
});
});
})}>
<Resource options={{ label: 'Categories' }} name="category" list={CategoryList}/>
</Admin>
);
console.log(wrapper.html());//<-- does not log anything
});
});
The original component
export const CategoryList = (props: any) => (
<List {...props} perPage={50}>
<Datagrid>
<TextField source="id" />
<TextField source="name" />
<TextField source="displaySequence" />
<EditButton/>
<ShowButton/>
</Datagrid>
</List>
);
Can some one please modify & suggest on how to mock the restClient using JEST ? I guess that is the place I am going wrong.
Also, is there a better way to test the list in isolation ?

As your restClient is async, you have to wait for the next tick to get something in return, see https://stackoverflow.com/a/43855794/1333479

Related

Guidance on implementing Okta Redirect with Docusaurus

Having trobule implementing okta redirect with docusaurus using their documentation due to how docusaurus intiallly loads in routes. Can anyone provide any guidance on how to go about this?
https://github.com/okta/okta-react
Expected Behavior:
Initial path to load up redirects to okta and authenticates then returns back to webpage.
I ran into this same issue and saw your posting hoping for an answer. bummer. Then I dug around a little more. It's not fully implemented yet because I'm waiting on the okta creds from my administrators, but this got me to a permission denied screen (which is a good thing to me!)
Few things:
docusaurus currently uses react-router-dom v5. you need to specifically set that instead of defaulting to v6
src/pages/index.tsx (i'm using typescript) should allow you to setup a browserrouter there
react-router-dom package:
"react-router": "^5.3.3",
"react-router-config": "^5.1.1",
"react-router-dom": "^5.3.3"
src/pages/index.tsx - I updated the home component to have a DocusaurusHome component, then made Home hold the routing logic
import React from 'react';
import clsx from 'clsx';
import Link from '#docusaurus/Link';
import useDocusaurusContext from '#docusaurus/useDocusaurusContext';
import Layout from '#theme/Layout';
import HomepageFeatures from '#site/src/components/HomepageFeatures';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import styles from './index.module.css';
import { OktaAuth } from '#okta/okta-auth-js';
import { Security, LoginCallback } from '#okta/okta-react';
import { RequiredAuth } from '../components/RequiredAuth';
// file with client id
import clientId from '../Okta/OktaClientID';
// file with issuer url
import issuerUrl from '../Okta/OktaIssuerUrl';
const config = {
clientId: clientId,
issuer: issuerUrl,
redirectUri: `${location.protocol}//${location.host}/callback`,
scopes: ['openid', 'profile', 'email'],
pkce: true
};
const oktaAuth = new OktaAuth(config);
function HomepageHeader() {
const {siteConfig} = useDocusaurusContext();
return (
<header className={clsx('hero hero--primary', styles.heroBanner)}>
<div className="container">
<h1 className="hero__title">{siteConfig.title}</h1>
<p className="hero__subtitle">{siteConfig.tagline}</p>
<div className={styles.buttons}>
<Link
className="button button--secondary button--lg"
to="/docs/intro">
Docusaurus Tutorial - 5min ⏱️
</Link>
</div>
</div>
</header>
);
}
/**
* Actual Docusaurus Home component
*/
function DocusaurusHome(): JSX.Element {
const {siteConfig} = useDocusaurusContext();
return (<Layout
title={`Hello from ${siteConfig.title}`}
description="Description will go into a meta tag in <head />">
<HomepageHeader />
<main>
<HomepageFeatures />
</main>
</Layout>)
}
/**
* component for react-router-dom browserrouter and okta auth
*/
export default function Home(): JSX.Element {
const restore = async (_oktaAuth: OktaAuth, originalUri: string) => {
window.location.replace(originalUri);
};
return (
<BrowserRouter>
<Security oktaAuth={oktaAuth} restoreOriginalUri={restore}>
<Switch>
<Route path='/callback'>
<LoginCallback
errorComponent={(err) => {
// eslint-disable-next-line no-console
console.error(err);
setTimeout(() => {
localStorage.removeItem('okta-token-storage');
window.location.replace(`${location.protocol}//${location.host}/`);
}, 2000);
return null;
}}/>
</Route>
<Route path='/'>
<RequiredAuth />
</Route>
<Route path='*'>
<DocusaurusHome />
</Route>
</Switch>
</Security>
</BrowserRouter>);
}
The RequiredAuth component originally returned an react-router-dom Outlet, which doesn't exist in v5. I think that a Route should suffice in it's place
import React, { useEffect } from 'react';
import { useOktaAuth } from '#okta/okta-react';
import { toRelativeUrl } from '#okta/okta-auth-js';
import { Route } from 'react-router-dom';
export const RequiredAuth: React.FC = () => {
const { oktaAuth, authState } = useOktaAuth();
useEffect(() => {
if (!authState) {
return;
}
if (!authState?.isAuthenticated) {
const originalUri = toRelativeUrl(window.location.href, window.location.origin);
oktaAuth.setOriginalUri(originalUri);
oktaAuth.signInWithRedirect();
}
}, [oktaAuth, !!authState, authState?.isAuthenticated]);
if (!authState || !authState?.isAuthenticated) {
return <></>; // loading screen before okta login
}
return (<Route />);
};

Text field stays empty

I'm trying to integrate a React-Toolbox Input component with Redux-Form. However, the Input component remains empty when typing. I'm using https://github.com/react-toolbox/react-toolbox/issues/1293 as a guide for the integration.
import React from 'react'
import PropTypes from 'prop-types'
import { Field, reduxForm } from 'redux-form'
import Input from 'react-toolbox/lib/input'
const renderField = ({ input, meta, ...props }) => (
<Input
{ ...input }
{ ...props }
error={ meta.touched && meta.error } />
)
const Form = ({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<Field
name="myTextField"
component={renderField}
type="text"
/>
</form>
)
Form.propTypes = {
handleSubmit: PropTypes.func.isRequired,
}
export default reduxForm({
form: 'myForm',
})(Form)
This is using react-toolbox 2.0.0-beta.12 and redux-form 7.2.0
You use input, meta and another ...props in your "functional component" renderField, but renderField props argument is named field and is not used anywhere.
You should change renderField this way:
const renderField = ({ input, meta, ...props }) => (
<Input
{ ...input }
{ ...props }
error={ meta.touched && meta.error }
/>
);
UPD
redux-form Basic Usage Guide says:
The redux store should know how to handle actions coming from the form components. To enable this, we need to pass the formReducer to your store. It serves for all of your form components, so you only have to pass it once.
So you should pass formReducer to your store:
import { createStore, combineReducers } from 'redux'
import { reducer as formReducer } from 'redux-form'
const rootReducer = combineReducers({
// ...your other reducers here
// you have to pass formReducer under 'form' key,
// for custom keys look up the docs for 'getFormState'
form: formReducer
})
const store = createStore(rootReducer)

How to richly style AOR Edit page

Have to create an edit Page editing a number of parameters on an instance of a'tale' resource.
However adding any element such as an MUI Card or even a div, is causing the app to freeze in various ways.
These are the approaches I have tried.
1) Adding a card component or placing my elements within a div for styling
export const EditorEditTale = (props) => {
return (
<Edit {...props} title="Tale Editor">
<SimpleForm >
<div>
<Image />
<TaleCardHeader props={ props } style={taleCardHeaderStyle.editor} />
</div>
</SimpleForm>
</Edit>
)
};
This is causing nothing to render.
Second approach, assuming that the record and basePath arent getting propagated to the children completely. Trying to use component like below.
const Input = ({record, basePath}) => {
return (
<div>
<LongTextInput source="taleText" />
</div>
)
}
This is causing the page to not render with everything in some kind of locking loop with the error - cannot read property touched of undefined.
How should I create a custom Edit page with a complex inputs and styling.
UPDATE: Been trying to write a custom form to substitute the SimpleForm component with no luck so far.
To create a custom form you can follow these steps:
make an exact copy of SimpleForm to your project.
rename SimpleForm to what you want.
fix all the relative imports.
test the new form until it works.
I made a minimum working form based on current master branch's SimpleForm
import React, { Children, Component } from 'react';
import PropTypes from 'prop-types';
import { reduxForm, Field } from 'redux-form';
import { connect } from 'react-redux';
import compose from 'recompose/compose';
import getDefaultValues from 'admin-on-rest/mui/form/getDefaultValues';
import FormField from 'admin-on-rest/mui/form/FormField';
import Toolbar from 'admin-on-rest/mui/form/Toolbar';
const formStyle = { padding: '0 1em 1em 1em' };
export class PostForm extends Component {
handleSubmitWithRedirect = (redirect = this.props.redirect) => this.props.handleSubmit(values => this.props.save(values, redirect));
render() {
const { children, invalid, record, resource, basePath, submitOnEnter, toolbar } = this.props;
return (
<form className="simple-form">
<Field name="name_of_a_field" component="input" />
{toolbar && React.cloneElement(toolbar, {
handleSubmitWithRedirect: this.handleSubmitWithRedirect,
invalid,
submitOnEnter,
})}
</form>
);
}
}
PostForm.propTypes = {
basePath: PropTypes.string,
children: PropTypes.node,
defaultValue: PropTypes.oneOfType([
PropTypes.object,
PropTypes.func,
]),
handleSubmit: PropTypes.func, // passed by redux-form
invalid: PropTypes.bool,
record: PropTypes.object,
resource: PropTypes.string,
redirect: PropTypes.oneOfType([
PropTypes.string,
PropTypes.bool,
]),
save: PropTypes.func, // the handler defined in the parent, which triggers the REST submission
submitOnEnter: PropTypes.bool,
toolbar: PropTypes.element,
validate: PropTypes.func,
};
PostForm.defaultProps = {
submitOnEnter: true,
toolbar: <Toolbar />,
};
const enhance = compose(
connect((state, props) => ({
initialValues: getDefaultValues(state, props),
})),
reduxForm({
form: 'record-form',
enableReinitialize: true,
}),
);
export default enhance(PostForm);
The above code works for AOR's example.
I hope this helps.
(import might be slightly different when you have AOR as npm dependency :
import getDefaultValues from 'admin-on-rest/lib/mui/form/getDefaultValues';
import FormField from 'admin-on-rest/lib/mui/form/FormField';
import Toolbar from 'admin-on-rest/lib/mui/form/Toolbar';
)
Documenting my final answer. You have to create a custom Redux Form. You can use AOR Input components straight. They come prewrapped for Redux Form.
import { Field, reduxForm } from 'redux-form';
import compose from 'recompose/compose';
import { connect } from 'react-redux';
class StyledForm extends Component {
// Newer version of aor needs this function defined and passed to save buttons. All props are being passed by parent List component.
handleSubmitWithRedirect = (redirect = this.props.redirect) => this.props.handleSubmit(values => this.props.save(values, redirect));
render() {
const { handleSubmit, invalid, record, resource, basePath } = this.props
return (<div>
<form onSubmit={handleSubmit} >
<Card >
<CardText >
//This component simply displays data, something not possible very easily with SimpleForm.
<HeaderComp basePath={basePath} record={record} />
<Field source="category_id"
optionText="categoryName"
reference="categories"
resource={resource}
record={record}
basePath={basePath}
name="NAME OF THE FIELD IN YOUR REDUX DATASTORE"
component={REFERENCEFIELDCOMP} />
//create complex div structures now.
<div >
<span>Tale</span>
<Field resource={resource} record={record} basePath={basePath} name="taleText" component={TextInput} />
</div>
</CardText >
<MuiToolbar>
<ToolbarGroup>
<SaveButton handleSubmitWithRedirect={this.handleSubmitWithRedirect}/>
//Add custom buttons with custom actions
<Field record={record} name="status" component={EditButtons} />
</ToolbarGroup>
</MuiToolbar>
</Card>
</form>
</div>)
}
};
const enhance = compose(
connect((state, props) => ({
initialValues: getDefaultValues(state, props),
})),
reduxForm({
form: 'record-form',
enableReinitialize: true,
}),
);
export default enhance(StyledForm);
You will have to either import or copy getDefaultValues from AOR in the node modules.
I copied it into the file below.
import getDefaultValues from '../functions/getDefaultValues';
If you need a referenceField in your field. Then wrap it in a custom component like shown below
const DropDownSelector = ({optionText, ...props}) => {
return (
<ReferenceInput {...props} label="" >
<SelectInput optionText={optionText} />
</ReferenceInput>
)
}

translate example is not working

I'm trying to use the example of translating your own component
so i'm doing this in app.js:
const messages = {
en: {
myroot: {
hello: {
world: 'Hello, World!',
},
},
},
};
const App = () => (
<Admin message={messages} locale="en" ...>
<Resource name="myresource" edit={EditPage} />
and in my Translation component:
import React from 'react';
import { translate } from 'admin-on-rest';
const Translation = ({ translate }) => (
<button>{translate('myroot.hello.world')}</button>
);
export default translate(Translation);
finally in my EditPage:
import Translation from 'path/to/Translation';
export const EditPage = (props) => (
<Edit {...props}>
<Translation />
</Edit>
);
its not working for me. its just showing myroot.hello.world in the button.
could you please help me out with that?
Typo ?
<Admin messages={messages} locale="en" ...>
Note that the prop is messages and not message

Values passed to redux-form initialValues are not rendered

I have a problem rendering initialValues passed to a form using redux-form/immutable (v6.0.5) with a custom component:
// WRAPPER COMPONENT
import React from 'react';
import {connect} from 'react-redux';
import DocumentsEditForm from './documentsEditForm';
import {documentsGetOneInfo} from './../../../modules/documents/actions/documents_get_one_info';
import {documentsPut} from './../../../modules/documents/actions/documents_put';
#connect((state) => {
return ({
selectedDocument: state.getIn(['documents', 'selectedDocument']).toJS()
});
})
class DocumentsEdit extends React.Component {
static propTypes = {
dispatch: React.PropTypes.func,
isLoading: React.PropTypes.bool,
routing: React.PropTypes.object,
selectedDocument: React.PropTypes.object
};
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}
componentWillMount() {
this.props.dispatch(adminDocumentsGetOneInfo(this.props.routing.locationBeforeTransitions.query.id));
}
handleSubmit(data) {
this.props.dispatch(documentsPut(data));
}
render() {
const initialValues = (this.props.selectedDocument.id) ? {
filename: this.props.selectedDocument.filename,
id: this.props.selectedDocument.id
} : {};
return (
<DocumentsEditForm
enableReinitialize
initialValues={initialValues}
onSubmitDocumentsEditForm={this.handleSubmit}
/>
);
}
}
export default DocumentsEdit;
Here is the form component itself:
import React from 'react';
import {Field, reduxForm} from 'redux-form/immutable';
import {Button} from 'react-bootstrap';
import InputTextRedux from './../../shared/components/input_text_redux';
import InputSelectRedux from './../../shared/components/input_select_redux';
let DocumentsEditForm = (props) => {
const {error, handleSubmit, pristine} = props;
return (
<form
onSubmit={handleSubmit(props.onSubmitDocumentsEditForm)}
>
<Field
component={InputTextRedux}
disabled
name="id"
type="text"
/>
<Field
component={InputTextRedux}
name="filename"
type="text"
/>
<Button
type="submit"
>
{'Save'}
</Button>
</form>
);
};
DocumentsEditForm.propTypes = {
error: React.PropTypes.object,
handleSubmit: React.PropTypes.func,
onSubmitDocumentsEditForm: React.PropTypes.func,
pristine: React.PropTypes.bool
};
documentsEditForm = reduxForm({
form: 'documentsEditForm'
})(DocumentsEditForm);
export default DocumentsEditForm;
And here is the custom InputTextRedux component:
import React from 'react';
import {ControlLabel, FormControl, FormGroup, InputGroup} from 'react-bootstrap';
const InputTextRedux = (props) => {
const {
id,
disabled,
input,
type
} = props;
return (
<div>
<FormGroup
name={input.name ? input.name : ''}
>
<ControlLabel>
{'Label'}
</ControlLabel>
<InputGroup>
<FormControl
disabled={disabled ? true : false}
id={id ? id : input.name}
onChange={(event) => {
input.onChange(event.target.value);
}}
type={type ? type : 'text'}
value={input.value ? input.value : ''}
/>
<FormControl.Feedback />
</InputGroup>
</FormGroup>
</div>
);
};
InputTextRedux.propTypes = {
disabled: React.PropTypes.bool,
id: React.PropTypes.string,
input: React.PropTypes.object,
type: React.PropTypes.string,
};
export default InputTextRedux;
I can submit the form and get the correct initialValues passed to handleSubmit but I can't seem to get the values displayed.
According to DevTools redux-form/INITIALIZE gets called with the correct payload and also the state at state->form->documentsEditForm->values/initial is correctly updated.
I also tried loading fixed initialValues to rule out problems with the api call delay but I got the same result. "input: {value: ''}" is always an empty string in the Field props.
What I noticed was that inside the props the form component recieves the structure looks as follows:
props {
....
initialValues: {
__altered: false,
_root: {
entries: [
[
'id',
'123456'
],
[
'filename',
'test.txt'
]
]
},
size: 2
},
....
}
If I try to set a field's value directly to "props.initialValues._root.entries[0][1] for example it works.
Also I noticed since the full form has a Field with name="size" that this value gets filled correctly with the value "2" from initialValues above. So it seems as if the form is looking at the wrong "level" of initialValues for the values. To test this I also tried naming a Field "__altered" and its value was correctly set to false.
What am I doing wrong? Thanks alot!
This has been resolved: See https://github.com/erikras/redux-form/issues/1744#issuecomment-251140419
Basically I had to import "redux-form/immutable" instead of "redux-form".

Resources