Skip to content

Commit

Permalink
Merge pull request #3409 from marmelab/useCreateController
Browse files Browse the repository at this point in the history
[RFR] Add useCreateController hook
  • Loading branch information
djhi authored Jul 13, 2019
2 parents 0b5597c + f88207f commit 022650c
Show file tree
Hide file tree
Showing 9 changed files with 283 additions and 207 deletions.
9 changes: 8 additions & 1 deletion examples/simple/src/users/UserCreate.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,14 @@ import {

import Aside from './Aside';

const UserEditToolbar = ({ permissions, ...props }) => (
const UserEditToolbar = ({
permissions,
hasList,
hasEdit,
hasShow,
hasCreate,
...props
}) => (
<Toolbar {...props}>
<SaveButton
label="user.action.save_and_show"
Expand Down
151 changes: 20 additions & 131 deletions packages/ra-core/src/controller/CreateController.tsx
Original file line number Diff line number Diff line change
@@ -1,146 +1,35 @@
import { ReactNode, useCallback } from 'react';
// @ts-ignore
import { useDispatch, useSelector } from 'react-redux';
import inflection from 'inflection';
import { parse } from 'query-string';

import { crudCreate } from '../actions';
import { useCheckMinimumRequiredProps } from './checkMinimumRequiredProps';
import { Location } from 'history';
import { match as Match } from 'react-router';
import { Record, Translate, ReduxState } from '../types';
import { RedirectionSideEffect } from '../sideEffect';
import { Translate } from '../types';
import { useTranslate } from '../i18n';
import useCreateController, {
CreateProps,
CreateControllerProps,
} from './useCreateController';

interface ChildrenFuncParams {
isLoading: boolean;
defaultTitle: string;
save: (record: Partial<Record>, redirect: RedirectionSideEffect) => void;
resource: string;
basePath: string;
record?: Partial<Record>;
redirect: RedirectionSideEffect;
interface CreateControllerComponentProps extends CreateControllerProps {
translate: Translate;
}

interface Props {
basePath: string;
children: (params: ChildrenFuncParams) => ReactNode;
hasCreate?: boolean;
hasEdit?: boolean;
hasList?: boolean;
hasShow?: boolean;
location: Location;
match: Match;
record?: Partial<Record>;
resource: string;
interface Props extends CreateProps {
children: (params: CreateControllerComponentProps) => JSX.Element;
}

/**
* Page component for the Create view
*
* The `<Create>` component renders the page title and actions.
* It is not responsible for rendering the actual form -
* that's the job of its child component (usually `<SimpleForm>`),
* to which it passes pass the `record` as prop.
*
* The `<Create>` component accepts the following props:
*
* - title
* - actions
*
* Both expect an element for value.
* Render prop version of the useCreateController hook
*
* @see useCreateController
* @example
* // in src/posts.js
* import React from 'react';
* import { Create, SimpleForm, TextInput } from 'react-admin';
*
* export const PostCreate = (props) => (
* <Create {...props}>
* <SimpleForm>
* <TextInput source="title" />
* </SimpleForm>
* </Create>
* );
*
* // in src/App.js
* import React from 'react';
* import { Admin, Resource } from 'react-admin';
*
* import { PostCreate } from './posts';
*
* const App = () => (
* <Admin dataProvider={...}>
* <Resource name="posts" create={PostCreate} />
* </Admin>
* );
* export default App;
* const CreateView = () => <div>...</div>
* const MyCreate = props => (
* <CreateController {...props}>
* {controllerProps => <CreateView {...controllerProps} {...props} />}
* </CreateController>
* );
*/
const CreateController = (props: Props) => {
useCheckMinimumRequiredProps(
'Create',
['basePath', 'location', 'resource', 'children'],
props
);
const {
basePath,
children,
resource,
location,
record = {},
hasShow,
hasEdit,
} = props;

const translate = useTranslate();
const dispatch = useDispatch();
const recordToUse = getRecord(location, record);
const isLoading = useSelector(
(state: ReduxState) => state.admin.loading > 0
);

const save = useCallback(
(data: Partial<Record>, redirect: RedirectionSideEffect) => {
dispatch(crudCreate(resource, data, basePath, redirect));
},
[resource, basePath] // eslint-disable-line react-hooks/exhaustive-deps
);

const resourceName = translate(`resources.${resource}.name`, {
smart_count: 1,
_: inflection.humanize(inflection.singularize(resource)),
});
const defaultTitle = translate('ra.page.create', {
name: `${resourceName}`,
});
return children({
isLoading,
defaultTitle,
save,
resource,
basePath,
record: recordToUse,
redirect: getDefaultRedirectRoute(hasShow, hasEdit),
translate,
});
const CreateController = ({ children, ...props }: Props) => {
const controllerProps = useCreateController(props);
const translate = useTranslate(); // injected for backwards compatibility
return children({ translate, ...controllerProps });
};

export default CreateController;

export const getRecord = ({ state, search }, record: any = {}) =>
state && state.record
? state.record
: search
? parse(search, { arrayFormat: 'bracket' })
: record;

const getDefaultRedirectRoute = (hasShow, hasEdit) => {
if (hasEdit) {
return 'edit';
}
if (hasShow) {
return 'show';
}
return 'list';
};
2 changes: 2 additions & 0 deletions packages/ra-core/src/controller/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import useSortState from './useSortState';
import usePaginationState from './usePaginationState';
import useListController from './useListController';
import useEditController from './useEditController';
import useCreateController from './useCreateController';
import useShowController from './useShowController';
import { useCheckMinimumRequiredProps } from './checkMinimumRequiredProps';
export {
Expand All @@ -24,6 +25,7 @@ export {
useCheckMinimumRequiredProps,
useListController,
useEditController,
useCreateController,
useShowController,
useRecordSelection,
useVersion,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getRecord } from './CreateController';
import { getRecord } from './useCreateController';

describe('CreateController', () => {
describe('useCreateController', () => {
describe('getRecord', () => {
const location = {
pathname: '/foo',
Expand Down
138 changes: 138 additions & 0 deletions packages/ra-core/src/controller/useCreateController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { useCallback } from 'react';
// @ts-ignore
import inflection from 'inflection';
import { parse } from 'query-string';

import { useCreate } from '../fetch';
import { useCheckMinimumRequiredProps } from './checkMinimumRequiredProps';
import { Location } from 'history';
import { match as Match } from 'react-router';
import { Record } from '../types';
import { RedirectionSideEffect } from '../sideEffect';
import { useTranslate } from '../i18n';

export interface CreateControllerProps {
isLoading: boolean;
isSaving: boolean;
defaultTitle: string;
save: (record: Partial<Record>, redirect: RedirectionSideEffect) => void;
resource: string;
basePath: string;
record?: Partial<Record>;
redirect: RedirectionSideEffect;
}

export interface CreateProps {
basePath: string;
hasCreate?: boolean;
hasEdit?: boolean;
hasList?: boolean;
hasShow?: boolean;
location: Location;
match: Match;
record?: Partial<Record>;
resource: string;
}

/**
* Prepare data for the Create view
*
* @param {Object} props The props passed to the Create component.
*
* @return {Object} controllerProps Fetched data and callbacks for the Create view
*
* @example
*
* import { useCreateController } from 'react-admin';
* import CreateView from './CreateView';
*
* const MyCreate = props => {
* const controllerProps = useCreateController(props);
* return <CreateView {...controllerProps} {...props} />;
* }
*/
const useCreateController = (props: CreateProps): CreateControllerProps => {
useCheckMinimumRequiredProps(
'Create',
['basePath', 'location', 'resource'],
props
);
const {
basePath,
resource,
location,
record = {},
hasShow,
hasEdit,
} = props;

const translate = useTranslate();
const recordToUse = getRecord(location, record);

const [create, { loading: isSaving }] = useCreate(
resource,
{}, // set by the caller
{
onSuccess: {
notification: {
body: 'ra.notification.created',
level: 'info',
messageArgs: {
smart_count: 1,
},
},
basePath,
},
onFailure: {
notification: {
body: 'ra.notification.http_error',
level: 'warning',
},
},
}
);

const save = useCallback(
(data: Partial<Record>, redirectTo = 'list') =>
create(null, { data }, { onSuccess: { redirectTo } }),
[create]
);

const resourceName = translate(`resources.${resource}.name`, {
smart_count: 1,
_: inflection.humanize(inflection.singularize(resource)),
});
const defaultTitle = translate('ra.page.create', {
name: `${resourceName}`,
});

return {
isLoading: false,
isSaving,
defaultTitle,
save,
resource,
basePath,
record: recordToUse,
redirect: getDefaultRedirectRoute(hasShow, hasEdit),
};
};

export default useCreateController;

export const getRecord = ({ state, search }, record: any = {}) =>
state && state.record
? state.record
: search
? parse(search, { arrayFormat: 'bracket' })
: record;

const getDefaultRedirectRoute = (hasShow, hasEdit) => {
if (hasEdit) {
return 'edit';
}
if (hasShow) {
return 'show';
}
return 'list';
};
2 changes: 2 additions & 0 deletions packages/ra-core/src/fetch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import withDataProvider from './withDataProvider';
import useGetOne from './useGetOne';
import useGetList from './useGetList';
import useUpdate from './useUpdate';
import useCreate from './useCreate';

export {
fetchUtils,
Expand All @@ -22,6 +23,7 @@ export {
useGetOne,
useGetList,
useUpdate,
useCreate,
useQueryWithStore,
withDataProvider,
};
Loading

0 comments on commit 022650c

Please sign in to comment.