-
-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3409 from marmelab/useCreateController
[RFR] Add useCreateController hook
- Loading branch information
Showing
9 changed files
with
283 additions
and
207 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
4 changes: 2 additions & 2 deletions
4
.../src/controller/CreateController.spec.tsx → ...c/controller/useCreateController.spec.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.