@7span/react-list 0.0.6 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/react-list.js +199 -198
- package/dist/react-list.umd.cjs +1 -1
- package/package.json +19 -10
- package/src/components/attributes.jsx +53 -0
- package/src/components/empty.jsx +22 -0
- package/src/components/error.jsx +27 -0
- package/src/components/go-to.jsx +58 -0
- package/src/components/initial-loader.jsx +25 -0
- package/src/components/items.jsx +50 -0
- package/src/components/list.jsx +361 -0
- package/src/components/load-more.jsx +39 -0
- package/src/components/loader.jsx +29 -0
- package/src/components/pagination.jsx +158 -0
- package/src/components/per-page.jsx +65 -0
- package/src/components/refresh.jsx +34 -0
- package/src/components/search.jsx +59 -0
- package/src/components/summary.jsx +51 -0
- package/src/components/utils.js +42 -0
- package/src/context/list-provider.jsx +55 -0
- package/src/index.js +15 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { useListContext } from "../context/list-provider";
|
|
3
|
+
import { hasActiveFilters } from "./utils";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ReactList component for handling data fetching, pagination, and state management
|
|
7
|
+
*/
|
|
8
|
+
const ReactList = ({
|
|
9
|
+
initialItems = [],
|
|
10
|
+
children,
|
|
11
|
+
endpoint,
|
|
12
|
+
page = 1,
|
|
13
|
+
perPage = 25,
|
|
14
|
+
sortBy = "",
|
|
15
|
+
sortOrder = "desc",
|
|
16
|
+
count = 0,
|
|
17
|
+
search = "",
|
|
18
|
+
filters = {},
|
|
19
|
+
attrs,
|
|
20
|
+
version = 1,
|
|
21
|
+
paginationMode = "pagination",
|
|
22
|
+
meta = {},
|
|
23
|
+
onResponse,
|
|
24
|
+
afterPageChange,
|
|
25
|
+
afterLoadMore,
|
|
26
|
+
}) => {
|
|
27
|
+
const { requestHandler, setListState, stateManager } = useListContext();
|
|
28
|
+
|
|
29
|
+
const initRef = useRef(false);
|
|
30
|
+
|
|
31
|
+
const isLoadMore = paginationMode === "loadMore";
|
|
32
|
+
|
|
33
|
+
const getContext = useCallback(
|
|
34
|
+
(currentState) => {
|
|
35
|
+
return {
|
|
36
|
+
endpoint,
|
|
37
|
+
version,
|
|
38
|
+
meta,
|
|
39
|
+
search: currentState?.search || search,
|
|
40
|
+
page: currentState?.page || page,
|
|
41
|
+
perPage: currentState?.perPage || perPage,
|
|
42
|
+
sortBy: currentState?.sortBy || sortBy,
|
|
43
|
+
sortOrder: currentState?.sortOrder || sortOrder,
|
|
44
|
+
filters: currentState?.filters || filters,
|
|
45
|
+
attrSettings: currentState?.attrSettings || {},
|
|
46
|
+
isRefresh: false,
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
[endpoint, version, meta, search, page, perPage, sortBy, sortOrder, filters]
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const getSavedState = useCallback(() => {
|
|
53
|
+
try {
|
|
54
|
+
const context = getContext();
|
|
55
|
+
const oldState = stateManager?.get?.(context);
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
page: oldState?.page,
|
|
59
|
+
perPage: oldState?.perPage,
|
|
60
|
+
sortBy: oldState?.sortBy,
|
|
61
|
+
sortOrder: oldState?.sortOrder,
|
|
62
|
+
search: oldState?.search,
|
|
63
|
+
attrSettings: oldState?.attrSettings,
|
|
64
|
+
filters: oldState?.filters,
|
|
65
|
+
};
|
|
66
|
+
} catch (err) {
|
|
67
|
+
console.error(err);
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
}, [getContext, stateManager]);
|
|
71
|
+
|
|
72
|
+
const initializeState = useCallback(() => {
|
|
73
|
+
const savedState = getSavedState();
|
|
74
|
+
|
|
75
|
+
let initialPage = page;
|
|
76
|
+
if (isLoadMore) {
|
|
77
|
+
initialPage = 1;
|
|
78
|
+
} else if (savedState.page != null) {
|
|
79
|
+
initialPage = savedState.page;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
page: initialPage,
|
|
84
|
+
perPage: savedState.perPage != null ? savedState.perPage : perPage,
|
|
85
|
+
sortBy: savedState.sortBy != null ? savedState.sortBy : sortBy,
|
|
86
|
+
sortOrder:
|
|
87
|
+
savedState.sortOrder != null ? savedState.sortOrder : sortOrder,
|
|
88
|
+
search: savedState.search != null ? savedState.search : search,
|
|
89
|
+
filters: savedState.filters != null ? savedState.filters : filters,
|
|
90
|
+
attrSettings: savedState.attrSettings || {},
|
|
91
|
+
items: initialItems,
|
|
92
|
+
selection: [],
|
|
93
|
+
error: null,
|
|
94
|
+
response: null,
|
|
95
|
+
count: 0,
|
|
96
|
+
isLoading: false,
|
|
97
|
+
initializingState: !initialItems.length,
|
|
98
|
+
confirmedPage: null,
|
|
99
|
+
};
|
|
100
|
+
}, [
|
|
101
|
+
getSavedState,
|
|
102
|
+
search,
|
|
103
|
+
page,
|
|
104
|
+
perPage,
|
|
105
|
+
sortBy,
|
|
106
|
+
sortOrder,
|
|
107
|
+
search,
|
|
108
|
+
filters,
|
|
109
|
+
isLoadMore,
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
// Initialize state with default values
|
|
113
|
+
const [state, setState] = useState(initializeState);
|
|
114
|
+
|
|
115
|
+
const updateStateManager = useCallback(
|
|
116
|
+
(stateToSave) => {
|
|
117
|
+
if (stateManager) {
|
|
118
|
+
const context = getContext(stateToSave);
|
|
119
|
+
stateManager?.set?.(context);
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
[stateManager, getContext]
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Fetch data from the API
|
|
127
|
+
* @param {Object} addContext - Additional context to pass to the request handler
|
|
128
|
+
* @param {Object} newState - New state to use for the request
|
|
129
|
+
*/
|
|
130
|
+
const fetchData = useCallback(
|
|
131
|
+
async (addContext = {}, newState = null) => {
|
|
132
|
+
// Only set loading state if not initializing
|
|
133
|
+
if (!state.initializingState) {
|
|
134
|
+
setState((prev) => ({ ...prev, error: null, isLoading: true }));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const currentState = newState || state;
|
|
139
|
+
const previousItems = newState?.items ?? state.items;
|
|
140
|
+
const res = await requestHandler({
|
|
141
|
+
endpoint,
|
|
142
|
+
version,
|
|
143
|
+
meta,
|
|
144
|
+
page: currentState.page,
|
|
145
|
+
perPage: currentState.perPage,
|
|
146
|
+
search: currentState.search,
|
|
147
|
+
sortBy: currentState.sortBy,
|
|
148
|
+
sortOrder: currentState.sortOrder,
|
|
149
|
+
filters: currentState.filters,
|
|
150
|
+
...addContext,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (onResponse) onResponse(res);
|
|
154
|
+
|
|
155
|
+
let newItems;
|
|
156
|
+
|
|
157
|
+
if (isLoadMore) {
|
|
158
|
+
newItems =
|
|
159
|
+
currentState.page === 1
|
|
160
|
+
? res.items
|
|
161
|
+
: [...previousItems, ...res.items];
|
|
162
|
+
if (afterLoadMore) afterLoadMore(res);
|
|
163
|
+
} else {
|
|
164
|
+
newItems = res.items;
|
|
165
|
+
if (afterPageChange) afterPageChange(res);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const updatedState = {
|
|
169
|
+
...currentState,
|
|
170
|
+
response: res,
|
|
171
|
+
selection: [],
|
|
172
|
+
// Append items for loadMore, replace for pagination
|
|
173
|
+
items:
|
|
174
|
+
isLoadMore && currentState.page > 1
|
|
175
|
+
? [...previousItems, ...res.items]
|
|
176
|
+
: res.items,
|
|
177
|
+
count: res.count,
|
|
178
|
+
initializingState: false,
|
|
179
|
+
isLoading: false,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
updateStateManager(updatedState);
|
|
183
|
+
|
|
184
|
+
setState(updatedState);
|
|
185
|
+
} catch (err) {
|
|
186
|
+
setState((prev) => ({
|
|
187
|
+
...prev,
|
|
188
|
+
error: err,
|
|
189
|
+
items: [],
|
|
190
|
+
count: 0,
|
|
191
|
+
initializingState: false,
|
|
192
|
+
isLoading: false,
|
|
193
|
+
}));
|
|
194
|
+
throw err;
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
[endpoint, version, isLoadMore, meta, requestHandler, state]
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Handlers for various actions (pagination, sorting, filtering, etc.)
|
|
202
|
+
*/
|
|
203
|
+
const handlers = useMemo(
|
|
204
|
+
() => ({
|
|
205
|
+
setPage: (value, addContext) => {
|
|
206
|
+
let newPage = value;
|
|
207
|
+
if (value === 0) {
|
|
208
|
+
newPage = "";
|
|
209
|
+
}
|
|
210
|
+
const newState = { ...state, page: newPage };
|
|
211
|
+
setState(newState);
|
|
212
|
+
if (newPage) fetchData(addContext, newState);
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
setPerPage: (value) => {
|
|
216
|
+
const newState = { ...state, perPage: value, page: 1 };
|
|
217
|
+
setState(newState);
|
|
218
|
+
fetchData({}, newState);
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
setSearch: (value) => {
|
|
222
|
+
// Only update if value changed to prevent unnecessary requests
|
|
223
|
+
if (value !== state.search) {
|
|
224
|
+
const newState = { ...state, search: value, page: 1 };
|
|
225
|
+
setState(newState);
|
|
226
|
+
fetchData({}, newState);
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
setSort: ({ by, order }) => {
|
|
231
|
+
const newState = { ...state, sortBy: by, sortOrder: order, page: 1 };
|
|
232
|
+
setState(newState);
|
|
233
|
+
fetchData({}, newState);
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
loadMore: () => {
|
|
237
|
+
const newState = { ...state, page: state.page + 1 };
|
|
238
|
+
setState(newState);
|
|
239
|
+
fetchData({}, newState);
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
clearFilters: () => {
|
|
243
|
+
const newState = { ...state, filters: filters, page: 1 };
|
|
244
|
+
setState(newState);
|
|
245
|
+
fetchData({}, newState);
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
refresh: (addContext = { isRefresh: true }) => {
|
|
249
|
+
if (isLoadMore) {
|
|
250
|
+
// For loadMore, reset to page 1
|
|
251
|
+
const newState = { ...state, page: 1, items: [] };
|
|
252
|
+
setState(newState);
|
|
253
|
+
fetchData(addContext, newState);
|
|
254
|
+
} else {
|
|
255
|
+
// For pagination, keep current page
|
|
256
|
+
fetchData(addContext);
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
setFilters: (filters) => {
|
|
261
|
+
const newState = { ...state, filters, page: 1 };
|
|
262
|
+
setState(newState);
|
|
263
|
+
fetchData({}, newState);
|
|
264
|
+
},
|
|
265
|
+
updateItemById: (item, id) => {
|
|
266
|
+
const newItems = state.items.map((i) => {
|
|
267
|
+
if (i.id === id) {
|
|
268
|
+
return { ...i, ...item };
|
|
269
|
+
}
|
|
270
|
+
return i;
|
|
271
|
+
});
|
|
272
|
+
setState((prev) => ({ ...prev, items: newItems }));
|
|
273
|
+
},
|
|
274
|
+
setSelection: (selection) => setState((prev) => ({ ...prev, selection })),
|
|
275
|
+
}),
|
|
276
|
+
[fetchData, isLoadMore, state]
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Memoized state for context to prevent unnecessary re-renders
|
|
281
|
+
*/
|
|
282
|
+
const memoizedState = useMemo(
|
|
283
|
+
() => ({
|
|
284
|
+
data: state.items,
|
|
285
|
+
response: state.response,
|
|
286
|
+
error: state.error,
|
|
287
|
+
count: state.count,
|
|
288
|
+
selection: state.selection,
|
|
289
|
+
pagination: {
|
|
290
|
+
page: state.page,
|
|
291
|
+
perPage: state.perPage,
|
|
292
|
+
hasMore: state.items.length < state.count,
|
|
293
|
+
},
|
|
294
|
+
loader: {
|
|
295
|
+
isLoading: state.isLoading,
|
|
296
|
+
initialLoading: state.initializingState,
|
|
297
|
+
},
|
|
298
|
+
sort: { sortBy: state.sortBy, sortOrder: state.sortOrder },
|
|
299
|
+
hasActiveFilters: hasActiveFilters(state.filters, filters),
|
|
300
|
+
search: state.search,
|
|
301
|
+
filters: state.filters,
|
|
302
|
+
attrs: attrs || Object.keys(state.items[0] || {}),
|
|
303
|
+
isEmpty: state.items.length === 0,
|
|
304
|
+
...handlers,
|
|
305
|
+
}),
|
|
306
|
+
[
|
|
307
|
+
state.items,
|
|
308
|
+
state.response,
|
|
309
|
+
state.error,
|
|
310
|
+
state.count,
|
|
311
|
+
state.selection,
|
|
312
|
+
state.page,
|
|
313
|
+
state.perPage,
|
|
314
|
+
state.isLoading,
|
|
315
|
+
state.initializingState,
|
|
316
|
+
state.sortBy,
|
|
317
|
+
state.sortOrder,
|
|
318
|
+
state.search,
|
|
319
|
+
state.filters,
|
|
320
|
+
handlers,
|
|
321
|
+
attrs,
|
|
322
|
+
]
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
useEffect(() => {
|
|
326
|
+
if (!state.initializingState) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (!initRef.current) {
|
|
330
|
+
initRef.current = true;
|
|
331
|
+
|
|
332
|
+
// Initialize state manager
|
|
333
|
+
if (stateManager?.init) {
|
|
334
|
+
const context = getContext(state);
|
|
335
|
+
stateManager.init(context);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (!initialItems.length) handlers.setPage(state.page);
|
|
339
|
+
}
|
|
340
|
+
}, []);
|
|
341
|
+
|
|
342
|
+
// Update list state in context
|
|
343
|
+
useEffect(() => {
|
|
344
|
+
setListState(memoizedState);
|
|
345
|
+
}, [
|
|
346
|
+
setListState,
|
|
347
|
+
state.items,
|
|
348
|
+
state.count,
|
|
349
|
+
state.error,
|
|
350
|
+
state.isLoading,
|
|
351
|
+
state.selection,
|
|
352
|
+
state.page,
|
|
353
|
+
state.perPage,
|
|
354
|
+
state.sortBy,
|
|
355
|
+
state.sortOrder,
|
|
356
|
+
]);
|
|
357
|
+
|
|
358
|
+
return typeof children === "function" ? children(memoizedState) : children;
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
export default ReactList;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { memo, useCallback, useMemo } from "react";
|
|
2
|
+
import { useListContext } from "../context/list-provider";
|
|
3
|
+
|
|
4
|
+
export const ReactListLoadMore = memo(({ children }) => {
|
|
5
|
+
const { listState } = useListContext();
|
|
6
|
+
const { data, count, pagination, setPage, loader, error } = listState;
|
|
7
|
+
const { page, perPage } = pagination;
|
|
8
|
+
const { isLoading } = loader;
|
|
9
|
+
|
|
10
|
+
const hasMoreItems = useMemo(
|
|
11
|
+
() => page * perPage < count,
|
|
12
|
+
[page, perPage, count]
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const loadMore = useCallback(() => {
|
|
16
|
+
if (hasMoreItems && !isLoading) {
|
|
17
|
+
setPage(page + 1);
|
|
18
|
+
}
|
|
19
|
+
}, [hasMoreItems, isLoading, setPage, page]);
|
|
20
|
+
|
|
21
|
+
const scope = useMemo(
|
|
22
|
+
() => ({
|
|
23
|
+
isLoading,
|
|
24
|
+
loadMore,
|
|
25
|
+
hasMoreItems,
|
|
26
|
+
}),
|
|
27
|
+
[isLoading, loadMore, hasMoreItems]
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
if (!data || data.length === 0) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (error) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return children(scope);
|
|
39
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { memo, useMemo } from "react";
|
|
2
|
+
import { useListContext } from "../context/list-provider";
|
|
3
|
+
|
|
4
|
+
export const ReactListLoader = memo(({ children, position = "overlay" }) => {
|
|
5
|
+
const { listState } = useListContext();
|
|
6
|
+
const { loader } = listState;
|
|
7
|
+
const { isLoading, initializingState } = loader;
|
|
8
|
+
|
|
9
|
+
const scope = useMemo(
|
|
10
|
+
() => ({
|
|
11
|
+
isLoading,
|
|
12
|
+
}),
|
|
13
|
+
[isLoading]
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
if (!initializingState && !isLoading) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div>
|
|
22
|
+
{children || (
|
|
23
|
+
<div>
|
|
24
|
+
<p>Loading...</p>
|
|
25
|
+
</div>
|
|
26
|
+
)}
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { memo, useMemo } from "react";
|
|
2
|
+
import { useListContext } from "../context/list-provider";
|
|
3
|
+
|
|
4
|
+
export const ReactListPagination = memo(
|
|
5
|
+
({
|
|
6
|
+
children,
|
|
7
|
+
pageLinks = 5,
|
|
8
|
+
renderFirst,
|
|
9
|
+
renderPrev,
|
|
10
|
+
renderPages,
|
|
11
|
+
renderPage,
|
|
12
|
+
renderNext,
|
|
13
|
+
renderLast,
|
|
14
|
+
}) => {
|
|
15
|
+
const { listState } = useListContext();
|
|
16
|
+
const { data, count, pagination, setPage, loader, error } = listState;
|
|
17
|
+
const { page, perPage } = pagination;
|
|
18
|
+
const { initialLoading, isLoading } = loader;
|
|
19
|
+
|
|
20
|
+
const paginationState = useMemo(() => {
|
|
21
|
+
const pagesCount = Math.ceil(count / perPage);
|
|
22
|
+
const halfWay = Math.floor(pageLinks / 2);
|
|
23
|
+
const hasNext = page * perPage < count;
|
|
24
|
+
const hasPrev = page !== 1;
|
|
25
|
+
return { pagesCount, halfWay, hasNext, hasPrev };
|
|
26
|
+
}, [count, perPage, page, pageLinks]);
|
|
27
|
+
|
|
28
|
+
const pagesToDisplay = useMemo(() => {
|
|
29
|
+
const { pagesCount, halfWay } = paginationState;
|
|
30
|
+
const pages = Array.from({ length: Math.min(pageLinks, pagesCount) });
|
|
31
|
+
|
|
32
|
+
if (page <= halfWay) {
|
|
33
|
+
return pages.map((_, index) => index + 1);
|
|
34
|
+
} else if (pagesCount - page < halfWay) {
|
|
35
|
+
return pages.map((_, index) => pagesCount - index).reverse();
|
|
36
|
+
} else {
|
|
37
|
+
return pages.map((_, index) => page - halfWay + index);
|
|
38
|
+
}
|
|
39
|
+
}, [page, pageLinks, paginationState]);
|
|
40
|
+
|
|
41
|
+
const navigation = useMemo(
|
|
42
|
+
() => ({
|
|
43
|
+
prev: () => setPage(page - 1),
|
|
44
|
+
next: () => setPage(page + 1),
|
|
45
|
+
first: () => setPage(1),
|
|
46
|
+
last: () => setPage(paginationState.pagesCount),
|
|
47
|
+
setPage: (newPage) => setPage(newPage),
|
|
48
|
+
}),
|
|
49
|
+
[setPage, page, paginationState.pagesCount]
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const scope = useMemo(
|
|
53
|
+
() => ({
|
|
54
|
+
page,
|
|
55
|
+
perPage,
|
|
56
|
+
count,
|
|
57
|
+
...paginationState,
|
|
58
|
+
pagesToDisplay,
|
|
59
|
+
...navigation,
|
|
60
|
+
}),
|
|
61
|
+
[page, perPage, count, paginationState, pagesToDisplay, navigation]
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
if (initialLoading) return null;
|
|
65
|
+
|
|
66
|
+
if (!data || data.length === 0) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (error) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (children) {
|
|
75
|
+
return children(scope);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className="react-list-pagination">
|
|
80
|
+
{renderFirst ? (
|
|
81
|
+
renderFirst(scope)
|
|
82
|
+
) : (
|
|
83
|
+
<button
|
|
84
|
+
type="button"
|
|
85
|
+
disabled={!paginationState.hasPrev}
|
|
86
|
+
onClick={navigation.first}
|
|
87
|
+
>
|
|
88
|
+
First
|
|
89
|
+
</button>
|
|
90
|
+
)}
|
|
91
|
+
|
|
92
|
+
{renderPrev ? (
|
|
93
|
+
renderPrev(scope)
|
|
94
|
+
) : (
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
disabled={!paginationState.hasPrev}
|
|
98
|
+
onClick={navigation.prev}
|
|
99
|
+
>
|
|
100
|
+
Prev
|
|
101
|
+
</button>
|
|
102
|
+
)}
|
|
103
|
+
|
|
104
|
+
{renderPages ? (
|
|
105
|
+
renderPages(scope)
|
|
106
|
+
) : (
|
|
107
|
+
<div>
|
|
108
|
+
{pagesToDisplay.map((pageNum) => {
|
|
109
|
+
const isActive = pageNum === page;
|
|
110
|
+
const pageScope = { ...scope, page: pageNum, isActive };
|
|
111
|
+
|
|
112
|
+
return renderPage ? (
|
|
113
|
+
renderPage(pageScope)
|
|
114
|
+
) : (
|
|
115
|
+
<div key={`page-${pageNum}`}>
|
|
116
|
+
{isActive ? (
|
|
117
|
+
<span>{pageNum}</span>
|
|
118
|
+
) : (
|
|
119
|
+
<button
|
|
120
|
+
type="button"
|
|
121
|
+
onClick={() => navigation.setPage(pageNum)}
|
|
122
|
+
>
|
|
123
|
+
{pageNum}
|
|
124
|
+
</button>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
})}
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
|
|
132
|
+
{renderNext ? (
|
|
133
|
+
renderNext(scope)
|
|
134
|
+
) : (
|
|
135
|
+
<button
|
|
136
|
+
type="button"
|
|
137
|
+
disabled={!paginationState.hasNext}
|
|
138
|
+
onClick={navigation.next}
|
|
139
|
+
>
|
|
140
|
+
Next
|
|
141
|
+
</button>
|
|
142
|
+
)}
|
|
143
|
+
|
|
144
|
+
{renderLast ? (
|
|
145
|
+
renderLast(scope)
|
|
146
|
+
) : (
|
|
147
|
+
<button
|
|
148
|
+
type="button"
|
|
149
|
+
disabled={!paginationState.hasNext}
|
|
150
|
+
onClick={navigation.last}
|
|
151
|
+
>
|
|
152
|
+
Last
|
|
153
|
+
</button>
|
|
154
|
+
)}
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { memo, useCallback, useMemo } from "react";
|
|
2
|
+
import { useListContext } from "../context/list-provider";
|
|
3
|
+
|
|
4
|
+
export const ReactListPerPage = memo(
|
|
5
|
+
({ children, options = [10, 25, 50, 100] }) => {
|
|
6
|
+
const { listState } = useListContext();
|
|
7
|
+
const { data, pagination, setPerPage, loader, error } = listState;
|
|
8
|
+
const { perPage } = pagination;
|
|
9
|
+
const { initialLoading } = loader;
|
|
10
|
+
|
|
11
|
+
const serializedOptions = useMemo(() => {
|
|
12
|
+
return options.map((item) => {
|
|
13
|
+
if (typeof item !== "object") {
|
|
14
|
+
return {
|
|
15
|
+
value: item,
|
|
16
|
+
label: item,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return item;
|
|
20
|
+
});
|
|
21
|
+
}, [options]);
|
|
22
|
+
|
|
23
|
+
const handlePerPageChange = useCallback(
|
|
24
|
+
(e) => {
|
|
25
|
+
setPerPage(Number(e.target.value));
|
|
26
|
+
},
|
|
27
|
+
[setPerPage]
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const scope = useMemo(
|
|
31
|
+
() => ({
|
|
32
|
+
perPage,
|
|
33
|
+
setPerPage,
|
|
34
|
+
options: serializedOptions,
|
|
35
|
+
}),
|
|
36
|
+
[perPage, setPerPage, serializedOptions]
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
if (initialLoading) return null;
|
|
40
|
+
|
|
41
|
+
if (!data || data.length === 0) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (error) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className="react-list-per-page">
|
|
51
|
+
{children ? (
|
|
52
|
+
children(scope)
|
|
53
|
+
) : (
|
|
54
|
+
<select value={perPage} onChange={handlePerPageChange}>
|
|
55
|
+
{serializedOptions.map((option) => (
|
|
56
|
+
<option key={`option-${option.value}`} value={option.value}>
|
|
57
|
+
{option.label} items per page
|
|
58
|
+
</option>
|
|
59
|
+
))}
|
|
60
|
+
</select>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { memo, useCallback, useMemo } from "react";
|
|
2
|
+
import { useListContext } from "../context/list-provider";
|
|
3
|
+
|
|
4
|
+
export const ReactListRefresh = memo(({ children }) => {
|
|
5
|
+
const { listState } = useListContext();
|
|
6
|
+
const { loader, refresh } = listState;
|
|
7
|
+
const { isLoading, initialLoading } = loader;
|
|
8
|
+
|
|
9
|
+
const handleRefresh = useCallback(() => {
|
|
10
|
+
refresh({ isRefresh: true });
|
|
11
|
+
}, [refresh]);
|
|
12
|
+
|
|
13
|
+
const scope = useMemo(
|
|
14
|
+
() => ({
|
|
15
|
+
isLoading,
|
|
16
|
+
refresh: handleRefresh,
|
|
17
|
+
}),
|
|
18
|
+
[isLoading, handleRefresh]
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
if (initialLoading) return null;
|
|
22
|
+
|
|
23
|
+
if (children) {
|
|
24
|
+
return children(scope);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="react-list-refresh">
|
|
29
|
+
<button onClick={handleRefresh} disabled={isLoading}>
|
|
30
|
+
{isLoading ? "Loading..." : "Refresh"}
|
|
31
|
+
</button>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
});
|