Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify the development of custom List views #4952

Merged
merged 36 commits into from
Jun 30, 2020
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
9668002
Add ListContext and migrate List to TypeScript
fzaninotto Jun 17, 2020
d8f552b
use useListContext instead of props in Empty
fzaninotto Jun 18, 2020
a43d879
use useListContext instead of props in ListToolbar
fzaninotto Jun 18, 2020
4034017
Update typescript, eslint & pritter to be able to use null coaelish o…
fzaninotto Jun 18, 2020
a69f573
add top, left, right, and bottom props to <List>
fzaninotto Jun 18, 2020
fe5fb04
Revert "Update typescript, eslint & pritter to be able to use null co…
fzaninotto Jun 18, 2020
79c7155
replace modern JS by nos so modern one
fzaninotto Jun 18, 2020
8307552
small fixes
fzaninotto Jun 18, 2020
e897f04
use useListContextinstead of props in BulkActionsToolbar
fzaninotto Jun 18, 2020
5c1c24b
Migrate Datagrid to TypeScript
fzaninotto Jun 18, 2020
a8c7a88
Make ReferenceManyField include a ListContext
fzaninotto Jun 18, 2020
8e091c4
Improve backward compatibility
fzaninotto Jun 19, 2020
1d4441c
use useListContext instead of props in ReferenceArrayField
fzaninotto Jun 19, 2020
f0eb78c
Merge branch 'next' into controller-context
fzaninotto Jun 19, 2020
4d65400
use useListContext instead of props in SimpleList
fzaninotto Jun 19, 2020
41fcc34
Remove top, bottom, left and right props
fzaninotto Jun 19, 2020
c7129e2
Test List context in simple example
fzaninotto Jun 22, 2020
097c20d
Fix warnings
fzaninotto Jun 22, 2020
8c9886a
Fix unit tests
fzaninotto Jun 22, 2020
441f835
Fix warnings
fzaninotto Jun 22, 2020
a4f2b25
Fix last test
fzaninotto Jun 22, 2020
c5a7e1a
Fix warnings in Pagination test
fzaninotto Jun 23, 2020
f62261c
Fix warnings in List tests
fzaninotto Jun 23, 2020
eb221e2
Fix warnings in Filter tests
fzaninotto Jun 23, 2020
3f84625
Convert Filter to TypeScript
fzaninotto Jun 23, 2020
ee7cf65
Try to make tests pass
fzaninotto Jun 23, 2020
60caa8b
Migrate SingleFieldList and ListGuesser to ListContext
fzaninotto Jun 24, 2020
bd11207
Rewrite List documentation to illustrate the new context
fzaninotto Jun 26, 2020
6016447
add basic integration tests for useListContext
fzaninotto Jun 28, 2020
aa8976a
improve jsDoc of ListContext and useListcontext
fzaninotto Jun 28, 2020
1d00e6f
review
fzaninotto Jun 28, 2020
ddad038
Apply suggestions from code review
fzaninotto Jun 29, 2020
152b2c8
use Existing state management hooks
fzaninotto Jun 29, 2020
34cebc4
review
fzaninotto Jun 29, 2020
4f05291
Merge branch 'controller-context' of github.com:marmelab/react-admin …
fzaninotto Jun 29, 2020
e6f67cd
Review
fzaninotto Jun 30, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions packages/ra-core/src/controller/ListContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { createContext } from 'react';
import { ListControllerProps } from './useListController';

/**
* Context to store the result of the useListController() hook.
*
* Use the useListContext() hook to read the context.
*
* @see useListController
* @see useListContext
*
* @example
*
* import { useListController, ListContext } from 'ra-core';
*
* const List = props => {
* const controllerProps = useListController(props);
* return (
* <ListContext.Provider value={controllerProps}>
* ...
* </ListContext.Provider>
* );
* };
*/
const ListContext = createContext<ListControllerProps>({
basePath: null,
currentSort: null,
data: null,
defaultTitle: null,
displayedFilters: null,
filterValues: null,
hasCreate: null,
hideFilter: null,
ids: null,
loaded: null,
loading: null,
onSelect: null,
onToggleItem: null,
onUnselectItems: null,
page: null,
perPage: null,
resource: null,
selectedIds: null,
setFilters: null,
setPage: null,
setPerPage: null,
setSort: null,
showFilter: null,
total: null,
});

ListContext.displayName = 'ListContext';

export default ListContext;
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import useSortState from '../useSortState';
import usePaginationState from '../usePaginationState';

interface ChildrenFuncParams {
basePath: string;
currentSort: Sort;
data: RecordMap;
ids: Identifier[];
loaded: boolean;
page: number;
perPage: number;
referenceBasePath: string;
setPage: (page: number) => void;
setPerPage: (perPage: number) => void;
setSort: (field: string) => void;
Expand Down Expand Up @@ -60,7 +60,7 @@ export const ReferenceManyFieldController: FunctionComponent<Props> = ({
data,
ids,
loaded,
referenceBasePath,
basePath: referenceBasePath,
total,
} = useReferenceManyFieldController({
resource,
Expand All @@ -82,7 +82,7 @@ export const ReferenceManyFieldController: FunctionComponent<Props> = ({
loaded,
page,
perPage,
referenceBasePath,
basePath: referenceBasePath,
setPage,
setPerPage,
setSort: setSortField,
Expand Down
153 changes: 141 additions & 12 deletions packages/ra-core/src/controller/field/useReferenceManyFieldController.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import get from 'lodash/get';
import { useCallback } from 'react';

import { Record, Sort, RecordMap, Identifier } from '../../types';
import { useSafeSetState, removeEmpty } from '../../util';
import { useGetManyReference } from '../../dataProvider';
import { useNotify } from '../../sideEffect';
import { Record, Sort, RecordMap, Identifier } from '../../types';

/**
* @typedef ReferenceManyProps
Expand All @@ -13,12 +15,30 @@ import { useNotify } from '../../sideEffect';
* @property {string | false} referenceBasePath base path of the related record
* @property {number} total records
*/
export interface ReferenceManyProps {
data: RecordMap;
export interface ReferenceManyProps<RecordType = Record> {
basePath: string;
currentSort: Sort;
data: RecordMap<RecordType>;
defaultTitle: string;
displayedFilters: any;
filterValues: any;
hasCreate: boolean;
hideFilter: (filterName: string) => void;
ids: Identifier[];
loaded: boolean;
loading: boolean;
referenceBasePath: string;
loaded: boolean;
onSelect: (ids: Identifier[]) => void;
onToggleItem: (id: Identifier) => void;
onUnselectItems: () => void;
page: number;
perPage: number;
resource: string;
selectedIds: Identifier[];
setFilters: (filters: any, displayedFilters: any) => void;
setPage: (page: number) => void;
setPerPage: (page: number) => void;
setSort: (sort: string, order?: string) => void;
showFilter: (filterName: string, defaultValue: any) => void;
total: number;
}

Expand Down Expand Up @@ -84,12 +104,105 @@ const useReferenceManyFieldController = ({
filter = defaultFilter,
source,
basePath,
page,
perPage,
sort = { field: 'id', order: 'DESC' },
page: initialPage,
perPage: initialPerPage,
sort: initialSort = { field: 'id', order: 'DESC' },
}: Options): ReferenceManyProps => {
const referenceId = get(record, source);
const notify = useNotify();

// pagination logic
const [page, setPage] = useSafeSetState<number>(initialPage);
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved
const [perPage, setPerPage] = useSafeSetState<number>(initialPerPage);

// sort logic
const [sort, setSortObject] = useSafeSetState<Sort>(initialSort);
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved
const setSort = useCallback(
(field: string, order: string = 'ASC') => {
setSortObject(previousState => ({
field,
order:
field === previousState.field
? previousState.order === 'ASC'
? 'DESC'
: 'ASC'
: order,
}));
setPage(1);
},
[setPage, setSortObject]
);

// selection logic
const [selectedIds, setSelectedIds] = useSafeSetState<Identifier[]>([]);
const onSelect = useCallback(
(newIds: Identifier[]) => {
setSelectedIds(newIds);
},
[setSelectedIds]
);
const onToggleItem = useCallback(
(id: Identifier) => {
setSelectedIds(previousState => {
const index = previousState.indexOf(id);
if (index > -1) {
return [
...previousState.slice(0, index),
...previousState.slice(index + 1),
];
} else {
return [...previousState, id];
}
});
},
[setSelectedIds]
);
const onUnselectItems = useCallback(() => {
setSelectedIds([]);
}, [setSelectedIds]);

// filter logic
const [displayedFilters, setDisplayedFilters] = useSafeSetState<{

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useFilterState ?

Copy link
Member Author

@fzaninotto fzaninotto Jun 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, this hook doesn't handle the complex logic of setting filters, displayed filters, and page at the same time, so I can't use it in this case.

[key: string]: boolean;
}>({});
const [filterValues, setFilterValues] = useSafeSetState<{
[key: string]: any;
}>(filter);
const hideFilter = useCallback(
(filterName: string) => {
setDisplayedFilters(previousState => {
const { [filterName]: _, ...newState } = previousState;
return newState;
});
setFilterValues(previousState => {
const { [filterName]: _, ...newState } = previousState;
return newState;
});
},
[setDisplayedFilters, setFilterValues]
);
const showFilter = useCallback(
(filterName: string, defaultValue: any) => {
setDisplayedFilters(previousState => ({
previousState,
[filterName]: true,
}));
setFilterValues(previousState => ({
previousState,
[filterName]: defaultValue,
}));
},
[setDisplayedFilters, setFilterValues]
);
const setFilters = useCallback(
(filters, displayedFilters) => {
setFilterValues(removeEmpty(filters));
setDisplayedFilters(displayedFilters);
setPage(1);
},
[setDisplayedFilters, setFilterValues, setPage]
);

const referenceId = get(record, source);
const { data, ids, total, loading, loaded } = useGetManyReference(
reference,
target,
Expand All @@ -109,14 +222,30 @@ const useReferenceManyFieldController = ({
}
);

const referenceBasePath = basePath.replace(resource, reference);

return {
basePath: basePath.replace(resource, reference),
currentSort: sort,
data,
defaultTitle: null,
displayedFilters,
filterValues,
hasCreate: false,
hideFilter,
ids,
loaded,
loading,
referenceBasePath,
onSelect,
onToggleItem,
onUnselectItems,
page,
perPage,
resource,
selectedIds,
setFilters,
setPage,
setPerPage,
setSort,
showFilter,
total,
};
};
Expand Down
4 changes: 4 additions & 0 deletions packages/ra-core/src/controller/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import CreateController from './CreateController';
import EditController from './EditController';
import ListController from './ListController';
import ListContext from './ListContext';
import ShowController from './ShowController';
import useRecordSelection from './useRecordSelection';
import useVersion from './useVersion';
Expand All @@ -13,6 +14,7 @@ import useListController, {
sanitizeListRestProps,
ListControllerProps,
} from './useListController';
import useListContext from './useListContext';
import useEditController, { EditControllerProps } from './useEditController';
import useCreateController, {
CreateControllerProps,
Expand All @@ -27,6 +29,7 @@ export {
CreateController,
EditController,
ListController,
ListContext,
ShowController,
useCheckMinimumRequiredProps,
useListController,
Expand All @@ -40,6 +43,7 @@ export {
useSortState,
usePaginationState,
useReference,
useListContext,
useListParams,
ListControllerProps,
EditControllerProps,
Expand Down
20 changes: 20 additions & 0 deletions packages/ra-core/src/controller/useListContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useContext } from 'react';

import ListContext from './ListContext';

/**
* Hook to read the list controller props.
*
* Must be used within a <ListContext.Provider>
*/
const useListContext = () => {
const context = useContext(ListContext);
if (!context.basePath) {
throw new Error(
'This component must be used inside a <ListContext.Provider>'
);
}
return context;
};

export default useListContext;
11 changes: 3 additions & 8 deletions packages/ra-core/src/controller/useListController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import useRecordSelection from './useRecordSelection';
import useVersion from './useVersion';
import { useTranslate } from '../i18n';
import { SORT_ASC } from '../reducer/admin/resource/list/queryReducer';
import { CRUD_GET_LIST, ListParams } from '../actions';
import { CRUD_GET_LIST } from '../actions';
import { useNotify } from '../sideEffect';
import { Sort, RecordMap, Identifier, ReduxState, Record } from '../types';
import useGetList from '../dataProvider/useGetList';
Expand All @@ -21,20 +21,18 @@ export interface ListProps {
filter?: object;
filters?: ReactElement<any>;
filterDefaultValues?: object;
pagination?: ReactElement<any>;
perPage?: number;
sort?: Sort;
// the props managed by react-admin
basePath: string;
basePath?: string;
debounce?: number;
hasCreate?: boolean;
hasEdit?: boolean;
hasList?: boolean;
hasShow?: boolean;
location?: Location;
path?: string;
query: ListParams;
resource: string;
resource?: string;
[key: string]: any;
}

Expand Down Expand Up @@ -70,7 +68,6 @@ export interface ListControllerProps<RecordType = Record> {
setSort: (sort: string, order?: string) => void;
showFilter: (filterName: string, defaultValue: any) => void;
total: number;
version: number;
}

/**
Expand Down Expand Up @@ -115,7 +112,6 @@ const useListController = <RecordType = Record>(
const location = useLocation();
const translate = useTranslate();
const notify = useNotify();
const version = useVersion();

const [query, queryModifiers] = useListParams({
resource,
Expand Down Expand Up @@ -225,7 +221,6 @@ const useListController = <RecordType = Record>(
setSort: queryModifiers.setSort,
showFilter: queryModifiers.showFilter,
total: typeof total === 'undefined' ? defaultTotal : total,
version,
};
};

Expand Down
2 changes: 1 addition & 1 deletion packages/ra-core/src/export/ExporterContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { createContext } from 'react';
import { Exporter } from '../types';
import defaultExporter from './defaultExporter';

const ExporterContext = createContext<Exporter>(defaultExporter);
const ExporterContext = createContext<Exporter | false>(defaultExporter);

ExporterContext.displayName = 'ExporterContext';

Expand Down
Loading