@ifto/mantine-kit 0.4.0 → 0.5.2
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/README.md +81 -10
- package/dist/component/organism/Table/Table.d.ts +9 -9
- package/dist/component/organism/Table/Table.js +21 -24
- package/dist/component/organism/Table/Table.js.map +1 -1
- package/dist/component/organism/Table/TableData.d.ts +24 -0
- package/dist/component/organism/Table/TableData.js +1 -0
- package/dist/component/organism/Table/useClientPagination.d.ts +21 -0
- package/dist/component/organism/Table/useClientPagination.js +43 -0
- package/dist/component/organism/Table/useClientPagination.js.map +1 -0
- package/dist/component/organism/Table/useServerPagination.d.ts +24 -0
- package/dist/component/organism/Table/useServerPagination.js +105 -0
- package/dist/component/organism/Table/useServerPagination.js.map +1 -0
- package/dist/component/organism/ViewTable/DataTableView.d.ts +39 -0
- package/dist/component/organism/ViewTable/DataTableView.js +52 -0
- package/dist/component/organism/ViewTable/DataTableView.js.map +1 -0
- package/dist/component/organism/ViewTable/TableState.d.ts +30 -0
- package/dist/component/organism/ViewTable/TableState.js +19 -0
- package/dist/component/organism/ViewTable/TableState.js.map +1 -0
- package/dist/component/organism/ViewTable/ViewTable.d.ts +11 -33
- package/dist/component/organism/ViewTable/ViewTable.js +16 -101
- package/dist/component/organism/ViewTable/ViewTable.js.map +1 -1
- package/dist/component/organism/ViewTable/useClientTableState.d.ts +25 -0
- package/dist/component/organism/ViewTable/useClientTableState.js +68 -0
- package/dist/component/organism/ViewTable/useClientTableState.js.map +1 -0
- package/dist/component/template/ListPage/ListPage.d.ts +9 -19
- package/dist/component/template/ListPage/ListPage.js +5 -14
- package/dist/component/template/ListPage/ListPage.js.map +1 -1
- package/dist/data/PageDataSource.d.ts +10 -0
- package/dist/data/PageDataSource.js +1 -0
- package/dist/data/PageRequest.d.ts +29 -0
- package/dist/data/PageRequest.js +1 -0
- package/dist/hook/useService.d.ts +3 -3
- package/dist/hook/useService.js +2 -0
- package/dist/hook/useService.js.map +1 -1
- package/dist/util/ArrayUtil.d.ts +4 -3
- package/dist/util/ArrayUtil.js +3 -2
- package/dist/util/ArrayUtil.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ Install the optional ones only if you use the components that need them:
|
|
|
49
49
|
| `@mantine/form` | the form templates (`FormPage`, `CreateFormPage`, `EditFormPage`) and `DataForm`, which take a `useForm` form |
|
|
50
50
|
| `@mantine/modals` | the confirmations of `Table` (delete), `ListPage` and `DataForm` |
|
|
51
51
|
| `@mantine/dates` and `dayjs` (its peer dependency) | `DateField` |
|
|
52
|
-
| `mantine-datatable` | `Table`, `ViewTable`, `
|
|
52
|
+
| `mantine-datatable` | `Table`, `ViewTable`, `DataTableView` |
|
|
53
53
|
| `echarts` | `EChart` and the chart types |
|
|
54
54
|
| `exceljs` | Excel export |
|
|
55
55
|
| `axios` | `AxiosHttpClient` |
|
|
@@ -117,11 +117,13 @@ module.exports = {
|
|
|
117
117
|
|
|
118
118
|
### CRUD list page
|
|
119
119
|
|
|
120
|
-
`ListPage`
|
|
121
|
-
object with `
|
|
120
|
+
`ListPage` lays out the header with the "Add" button, optional filters and a table. The `Table` shows the records with
|
|
121
|
+
view, edit and delete actions. Its `service` is any object with `delete`, and its `data` comes from a pagination hook:
|
|
122
122
|
|
|
123
123
|
```tsx
|
|
124
124
|
import ListPage from '@ifto/mantine-kit/component/template/ListPage/ListPage';
|
|
125
|
+
import Table from '@ifto/mantine-kit/component/organism/Table/Table';
|
|
126
|
+
import { useClientPagination } from '@ifto/mantine-kit/component/organism/Table/useClientPagination';
|
|
125
127
|
import type { ListDataSource } from '@ifto/mantine-kit/data/ListDataSource';
|
|
126
128
|
|
|
127
129
|
type Region = { id?: number; name: string };
|
|
@@ -132,20 +134,88 @@ const regions: ListDataSource<Region> = {
|
|
|
132
134
|
};
|
|
133
135
|
|
|
134
136
|
export function RegionsPage() {
|
|
137
|
+
// The object must be stable (here, a module constant), since a new one fetches again.
|
|
138
|
+
const data = useClientPagination(regions);
|
|
139
|
+
|
|
135
140
|
return (
|
|
136
141
|
<ListPage
|
|
137
142
|
title="Regions"
|
|
138
143
|
titlePlural="Regions"
|
|
139
144
|
breadcrumbItems={[{ title: 'Regions', href: '/region' }]}
|
|
140
145
|
addActionRoute="/region/create"
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
146
|
+
>
|
|
147
|
+
<Table
|
|
148
|
+
data={data}
|
|
149
|
+
service={regions}
|
|
150
|
+
basePath="/region"
|
|
151
|
+
columns={[{ accessor: 'name', title: 'Name', sortable: true }]}
|
|
152
|
+
/>
|
|
153
|
+
</ListPage>
|
|
145
154
|
);
|
|
146
155
|
}
|
|
147
156
|
```
|
|
148
157
|
|
|
158
|
+
#### Client-side or server-side pagination
|
|
159
|
+
|
|
160
|
+
The hook that creates the `data` of the `Table` defines where the records are sorted and paginated:
|
|
161
|
+
|
|
162
|
+
| | `useClientPagination(source, { pagination })` | `useServerPagination(source, { firstPage })` |
|
|
163
|
+
|---|---|---|
|
|
164
|
+
| **Fetch** | all records at once | one page, with page, size and sort |
|
|
165
|
+
| **Sorted and paginated by** | browser (sorting follows the locale) | backend |
|
|
166
|
+
| **Total of records** | number of fetched records | `totalRecords` of the response |
|
|
167
|
+
|
|
168
|
+
Both take a `source`, which is either:
|
|
169
|
+
|
|
170
|
+
- an object with the method that fetches the records: `findAll()` (a `ListDataSource`) or `findPage(request)` (a
|
|
171
|
+
`PageDataSource`), such as a service. The hook calls the method on the object, so it keeps its `this`;
|
|
172
|
+
- or a function that fetches them, for fetches with a filter (e.g. `useCallback(request => service.findPageByYear(year,
|
|
173
|
+
request), [year])`).
|
|
174
|
+
|
|
175
|
+
The source must be stable (e.g. a module constant, or a function created with `useCallback`), since a new one fetches
|
|
176
|
+
again: a `new Service()` inside the component would fetch on every render.
|
|
177
|
+
|
|
178
|
+
`findPage` (or the function) receives the page, the size and the sort, and returns the records of that page and the total of records. The
|
|
179
|
+
contract knows nothing about your backend, so convert it to the format of your API (here, Spring Data, returning a
|
|
180
|
+
`PagedModel`, which puts the total of records in `page.totalElements`). The page starts at 1, or at the `firstPage`
|
|
181
|
+
option (e.g. 0 for Spring), which only changes the requests: the table always shows the pages starting at 1.
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
import { useCallback } from 'react';
|
|
185
|
+
import { useServerPagination } from '@ifto/mantine-kit/component/organism/Table/useServerPagination';
|
|
186
|
+
import type { FetchPage } from '@ifto/mantine-kit/data/PageRequest';
|
|
187
|
+
|
|
188
|
+
export function RegionsTable({ name }: { name: string }) {
|
|
189
|
+
// A new function means the filter changed, which goes back to page 1.
|
|
190
|
+
const fetchPage: FetchPage<Region> = useCallback(
|
|
191
|
+
async ({ page, size, sort }) => {
|
|
192
|
+
const params = new URLSearchParams({
|
|
193
|
+
name,
|
|
194
|
+
page: String(page),
|
|
195
|
+
size: String(size),
|
|
196
|
+
sort: `${String(sort.columnAccessor)},${sort.direction}`
|
|
197
|
+
});
|
|
198
|
+
const response = await fetch(`/api/region?${params}`).then(result => result.json());
|
|
199
|
+
|
|
200
|
+
return { records: response.content, totalRecords: response.page.totalElements };
|
|
201
|
+
},
|
|
202
|
+
[name]
|
|
203
|
+
);
|
|
204
|
+
const data = useServerPagination(fetchPage, { firstPage: 0 });
|
|
205
|
+
|
|
206
|
+
return <Table data={data} service={regions} basePath="/region" columns={columns} />;
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
With `useServerPagination`:
|
|
211
|
+
|
|
212
|
+
- `sort.columnAccessor` holds the `sortKey` of the column, if it has one, or else its `accessor`. A calculated column
|
|
213
|
+
needs a `sortKey` or `sortable: false`.
|
|
214
|
+
- The table goes back to page 1 when the page size, the sort or the filter changes, and to the last existing page when
|
|
215
|
+
the current one no longer exists (e.g. its last record was deleted).
|
|
216
|
+
- The order of null values and the text collation are defined by the backend, so they can differ from client-side
|
|
217
|
+
pagination, which follows the locale and always puts null values last.
|
|
218
|
+
|
|
149
219
|
### CRUD form page
|
|
150
220
|
|
|
151
221
|
`FormPage` creates, edits or views a record and saves it with any object that has a `save(id, data)` method:
|
|
@@ -395,9 +465,10 @@ Every file is imported by its path inside `src`, without the extension, e.g.
|
|
|
395
465
|
| `Navbar` | Side menu with links and collapsible groups, highlighting the current page. |
|
|
396
466
|
| `PageHeader` | Page title with the breadcrumb and, in list pages, the "Add" button. |
|
|
397
467
|
| `StatsGrid` | Responsive grid of `StatsCard`s, with placeholders while loading. |
|
|
398
|
-
| `Table` | Table of
|
|
468
|
+
| `Table` | Table of records with view, edit and delete (with confirmation) actions, paginated by `useClientPagination` or `useServerPagination`. |
|
|
399
469
|
| `RowActions` | View, edit and delete buttons of a `Table` row. |
|
|
400
|
-
| `ViewTable` |
|
|
470
|
+
| `ViewTable` | Table that sorts (locale-aware) and paginates the given records in the browser (on top of mantine-datatable). |
|
|
471
|
+
| `DataTableView` | Table that shows records as received, with the pagination and sort given by the caller. |
|
|
401
472
|
|
|
402
473
|
The `EChart/chart` folder has chart types that build the ECharts option from generic series: `LineChart`, `BarChart`
|
|
403
474
|
(negative values highlighted), `StackedBarChart` and `DonutChart`, with one panel per unit of measure.
|
|
@@ -407,7 +478,7 @@ The `EChart/chart` folder has chart types that build the ECharts option from gen
|
|
|
407
478
|
| Component | What it does |
|
|
408
479
|
|---|---|
|
|
409
480
|
| `Layout` | App shell: header with the brand, color scheme toggle and user menu, collapsible side menu and content area. |
|
|
410
|
-
| `ListPage` | List page of records: header with the "Add" button, optional filters and the
|
|
481
|
+
| `ListPage` | List page of records: header with the "Add" button, optional filters and the table given as children. |
|
|
411
482
|
| `FormPage` | Create, edit or view form page that saves the record in a `SaveDataSource`. |
|
|
412
483
|
| `CreateFormPage` | Form page to create a record with a given function. |
|
|
413
484
|
| `EditFormPage` | Form page to update a record with a given function. |
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ReactElement } from 'react';
|
|
2
2
|
import { DataTableColumn } from 'mantine-datatable';
|
|
3
|
+
import { TableData } from './TableData';
|
|
3
4
|
import { ListDataSource } from '../../../data/ListDataSource';
|
|
4
5
|
import { Identifiable } from '../../../model/Identifiable';
|
|
5
6
|
/** Which actions the table shows for each record. */
|
|
@@ -15,21 +16,19 @@ export type ActionsSettings = {
|
|
|
15
16
|
};
|
|
16
17
|
/** Props of the {@link Table}. */
|
|
17
18
|
type TableProps<M extends Identifiable, S extends ListDataSource<M>> = {
|
|
18
|
-
/** Source of the records and of their deletion (e.g. a service that accesses an API). */
|
|
19
|
-
service: S;
|
|
20
19
|
/**
|
|
21
|
-
*
|
|
22
|
-
* `(
|
|
20
|
+
* Records and their pagination and sort, given by `useClientPagination` (sorted and paginated in the browser) or
|
|
21
|
+
* `useServerPagination` (sorted and paginated by the backend).
|
|
23
22
|
*/
|
|
24
|
-
|
|
23
|
+
data: TableData<M>;
|
|
24
|
+
/** Source of the records, which deletes them (e.g. a service that accesses an API). */
|
|
25
|
+
service: S;
|
|
25
26
|
/** Columns of the table. */
|
|
26
27
|
columns: DataTableColumn<M>[];
|
|
27
28
|
/** Base path of the pages of the records (e.g. `/region`), used to build the view and edit URLs. */
|
|
28
29
|
basePath: string;
|
|
29
30
|
/** Which actions are shown; the ones not given are shown. */
|
|
30
31
|
actionsSettings?: Partial<ActionsSettings>;
|
|
31
|
-
/** Whether the table is paginated. */
|
|
32
|
-
pagination?: boolean;
|
|
33
32
|
/**
|
|
34
33
|
* Called by the edit action instead of opening the edit page of the record.
|
|
35
34
|
* @param itemId id of the record to edit
|
|
@@ -42,8 +41,9 @@ type TableProps<M extends Identifiable, S extends ListDataSource<M>> = {
|
|
|
42
41
|
getViewUrl?: (item: M) => string;
|
|
43
42
|
};
|
|
44
43
|
/**
|
|
45
|
-
* A table that shows the records of a data source, with the view, edit and delete actions
|
|
46
|
-
* record.
|
|
44
|
+
* A table that shows the records of a data source, with the view, edit and delete actions, or an alert when they cannot
|
|
45
|
+
* be fetched. Clicking a row opens the record. Its `data` comes from `useClientPagination` or `useServerPagination`,
|
|
46
|
+
* which define where the records are sorted and paginated.
|
|
47
47
|
* @template M type of the records, shown one per row
|
|
48
48
|
* @template S source of the records (e.g. a service that accesses an API)
|
|
49
49
|
*/
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import
|
|
2
|
+
import DataTableView from "../ViewTable/DataTableView.js";
|
|
3
3
|
import RowActions from "./RowActions.js";
|
|
4
4
|
import { useKitConfig } from "../../../provider/useKit.js";
|
|
5
5
|
import ErrorAlert from "../../atom/ErrorAlert/ErrorAlert.js";
|
|
6
|
-
import useService from "../../../hook/useService.js";
|
|
7
6
|
import { showErrorNotification, showSuccessNotification } from "../../../util/Notifications.js";
|
|
8
|
-
import {
|
|
7
|
+
import { useMemo } from "react";
|
|
9
8
|
import { jsx } from "react/jsx-runtime";
|
|
10
9
|
import { Box } from "@mantine/core";
|
|
11
10
|
// src/component/organism/Table/Table.tsx
|
|
12
11
|
var ROW_HOVER_COLOR = "var(--mantine-primary-color-light)";
|
|
13
|
-
/** Initial records of the table, while they are fetched; a single constant, so it is stable across renders. */
|
|
14
|
-
var NO_RECORDS = [];
|
|
15
12
|
/** Every action shown, used for the settings the caller does not give. */
|
|
16
13
|
var DEFAULT_ACTIONS_SETTINGS = {
|
|
17
14
|
canShowActions: true,
|
|
@@ -30,13 +27,13 @@ function viewUrl(props, item, segments) {
|
|
|
30
27
|
}
|
|
31
28
|
/**
|
|
32
29
|
* Creates the function that deletes a record and reloads the table.
|
|
33
|
-
* @param setData function that sets the records of the table
|
|
30
|
+
* @param setData function that sets the records of the table
|
|
34
31
|
* @param service source of the records, which deletes them
|
|
35
|
-
* @param
|
|
32
|
+
* @param reload fetches the records again after the deletion
|
|
36
33
|
* @param messages texts of the locale
|
|
37
34
|
* @return a function that receives the record to delete and returns a promise resolved when it is deleted
|
|
38
35
|
*/
|
|
39
|
-
function getDeleteRecordFn(setData, service,
|
|
36
|
+
function getDeleteRecordFn(setData, service, reload, messages) {
|
|
40
37
|
return async (item) => {
|
|
41
38
|
const id = item?.id ?? 0;
|
|
42
39
|
if (!id) {
|
|
@@ -46,33 +43,32 @@ function getDeleteRecordFn(setData, service, fetchMethod, messages) {
|
|
|
46
43
|
try {
|
|
47
44
|
await service.delete(id);
|
|
48
45
|
showSuccessNotification(messages.table.deleteSuccess, messages.common.success);
|
|
49
|
-
|
|
46
|
+
await reload();
|
|
50
47
|
} catch (e) {
|
|
51
48
|
showErrorNotification(e, void 0, messages.common.error);
|
|
52
49
|
}
|
|
53
50
|
};
|
|
54
51
|
}
|
|
55
52
|
/**
|
|
56
|
-
* A table that shows the records of a data source, with the view, edit and delete actions
|
|
57
|
-
* record.
|
|
53
|
+
* A table that shows the records of a data source, with the view, edit and delete actions, or an alert when they cannot
|
|
54
|
+
* be fetched. Clicking a row opens the record. Its `data` comes from `useClientPagination` or `useServerPagination`,
|
|
55
|
+
* which define where the records are sorted and paginated.
|
|
58
56
|
* @template M type of the records, shown one per row
|
|
59
57
|
* @template S source of the records (e.g. a service that accesses an API)
|
|
60
58
|
*/
|
|
61
59
|
var Table = (props) => {
|
|
62
|
-
const {
|
|
63
|
-
const
|
|
60
|
+
const { service, columns, basePath, customEditFunction, data } = props;
|
|
61
|
+
const { setRecords, reload } = data;
|
|
64
62
|
const { router, messages, formRouteSegments } = useKitConfig();
|
|
65
63
|
const navigate = router.useNavigate();
|
|
66
64
|
const settings = {
|
|
67
65
|
...DEFAULT_ACTIONS_SETTINGS,
|
|
68
66
|
...props.actionsSettings
|
|
69
67
|
};
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
useEffect(() => setData(fetchedData), [fetchedData]);
|
|
73
|
-
const deleteRecord = useMemo(() => getDeleteRecordFn(setData, service, fetchMethod, messages), [
|
|
68
|
+
const deleteRecord = useMemo(() => getDeleteRecordFn(setRecords, service, reload, messages), [
|
|
69
|
+
setRecords,
|
|
74
70
|
service,
|
|
75
|
-
|
|
71
|
+
reload,
|
|
76
72
|
messages
|
|
77
73
|
]);
|
|
78
74
|
const actionsColumn = {
|
|
@@ -91,15 +87,16 @@ var Table = (props) => {
|
|
|
91
87
|
})
|
|
92
88
|
};
|
|
93
89
|
const effectiveColumns = settings.canShowActions ? [...columns, actionsColumn] : columns;
|
|
94
|
-
if (error) return /* @__PURE__ */ jsx(ErrorAlert, {
|
|
90
|
+
if (data.error) return /* @__PURE__ */ jsx(ErrorAlert, {
|
|
95
91
|
title: messages.common.loadError,
|
|
96
|
-
message: String(error)
|
|
92
|
+
message: String(data.error)
|
|
97
93
|
});
|
|
98
|
-
return /* @__PURE__ */ jsx(
|
|
99
|
-
data,
|
|
94
|
+
return /* @__PURE__ */ jsx(DataTableView, {
|
|
95
|
+
records: data.records,
|
|
96
|
+
state: data.tableState,
|
|
97
|
+
paginated: data.paginated,
|
|
100
98
|
columns: effectiveColumns,
|
|
101
|
-
loading,
|
|
102
|
-
pagination: props.pagination,
|
|
99
|
+
loading: data.loading,
|
|
103
100
|
highlightOnHover: true,
|
|
104
101
|
highlightOnHoverColor: ROW_HOVER_COLOR,
|
|
105
102
|
onRowClick: ({ record }) => navigate(viewUrl(props, record, formRouteSegments))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Table.js","names":[],"sources":["../../../../src/component/organism/Table/Table.tsx"],"sourcesContent":["'use client';\n\nimport { ReactElement,
|
|
1
|
+
{"version":3,"file":"Table.js","names":[],"sources":["../../../../src/component/organism/Table/Table.tsx"],"sourcesContent":["'use client';\n\nimport { ReactElement, useMemo } from 'react';\n\nimport { Box } from '@mantine/core';\nimport { DataTableColumn } from 'mantine-datatable';\n\nimport DataTableView from '../ViewTable/DataTableView';\nimport { SetRecords, TableData } from './TableData';\nimport RowActions from './RowActions';\nimport { ListDataSource } from '../../../data/ListDataSource';\nimport { KitMessages } from '../../../i18n/KitMessages';\nimport { Identifiable } from '../../../model/Identifiable';\nimport { useKitConfig } from '../../../provider/useKit';\nimport { FormRouteSegments } from '../../../router/FormRouteSegments';\nimport ErrorAlert from '../../atom/ErrorAlert/ErrorAlert';\nimport { showErrorNotification, showSuccessNotification } from '../../../util/Notifications';\n\n/*\nColor of the row under the mouse. The default color of mantine-datatable (gray-1) is barely distinguishable from the\nzebra stripes (gray-0), so the highlight went unnoticed. The light variant of the primary color adapts to the light and\ndark color schemes.\n*/\nconst ROW_HOVER_COLOR = 'var(--mantine-primary-color-light)';\n\n/** Which actions the table shows for each record. */\nexport type ActionsSettings = {\n /** Whether the actions column is shown. */\n canShowActions: boolean;\n\n /** Whether the view action is shown. */\n canView: boolean;\n\n /** Whether the edit action is shown. */\n canEdit: boolean;\n\n /** Whether the delete action is shown. */\n canDelete: boolean;\n};\n\n/** Every action shown, used for the settings the caller does not give. */\nconst DEFAULT_ACTIONS_SETTINGS: ActionsSettings = {\n canShowActions: true,\n canView: true,\n canEdit: true,\n canDelete: true\n};\n\n/** Props of the {@link Table}. */\ntype TableProps<M extends Identifiable, S extends ListDataSource<M>> = {\n /**\n * Records and their pagination and sort, given by `useClientPagination` (sorted and paginated in the browser) or\n * `useServerPagination` (sorted and paginated by the backend).\n */\n data: TableData<M>;\n\n /** Source of the records, which deletes them (e.g. a service that accesses an API). */\n service: S;\n\n /** Columns of the table. */\n columns: DataTableColumn<M>[];\n\n /** Base path of the pages of the records (e.g. `/region`), used to build the view and edit URLs. */\n basePath: string;\n\n /** Which actions are shown; the ones not given are shown. */\n actionsSettings?: Partial<ActionsSettings>;\n\n /**\n * Called by the edit action instead of opening the edit page of the record.\n * @param itemId id of the record to edit\n */\n customEditFunction?: (itemId: M['id']) => void;\n\n /**\n * Builds the URL opened to view a record (click on the row or on the view icon).\n * If absent, the default route is used: `${basePath}/<view segment>/${item.id}`.\n */\n getViewUrl?: (item: M) => string;\n};\n\n/**\n * Returns the view URL of a record: the one of `getViewUrl`, if given; otherwise, the default view route.\n * @param props props of the table\n * @param item record\n * @param segments URL segments of the form pages\n */\nfunction viewUrl<M extends Identifiable, S extends ListDataSource<M>>(\n props: TableProps<M, S>,\n item: M,\n segments: FormRouteSegments\n): string {\n return props.getViewUrl ? props.getViewUrl(item) : `${props.basePath}/${segments.view}/${item.id}`;\n}\n\n/**\n * Creates the function that deletes a record and reloads the table.\n * @param setData function that sets the records of the table\n * @param service source of the records, which deletes them\n * @param reload fetches the records again after the deletion\n * @param messages texts of the locale\n * @return a function that receives the record to delete and returns a promise resolved when it is deleted\n */\nfunction getDeleteRecordFn<M extends Identifiable, S extends ListDataSource<M>>(\n setData: SetRecords<M>,\n service: S,\n reload: () => Promise<void>,\n messages: KitMessages\n): (item: M) => Promise<void> {\n return async (item: M) => {\n const id = item?.id ?? 0;\n\n // Without an id, the record was added only to the array and was not sent to the backend: just remove it.\n if (!id) {\n setData(prevData => prevData.filter(current => current !== item));\n return;\n }\n\n try {\n await service.delete(id);\n showSuccessNotification(messages.table.deleteSuccess, messages.common.success);\n await reload();\n } catch (e) {\n showErrorNotification(e, undefined, messages.common.error);\n }\n };\n}\n\n/**\n * A table that shows the records of a data source, with the view, edit and delete actions, or an alert when they cannot\n * be fetched. Clicking a row opens the record. Its `data` comes from `useClientPagination` or `useServerPagination`,\n * which define where the records are sorted and paginated.\n * @template M type of the records, shown one per row\n * @template S source of the records (e.g. a service that accesses an API)\n */\nconst Table = <M extends Identifiable, S extends ListDataSource<M>>(props: TableProps<M, S>): ReactElement => {\n const { service, columns, basePath, customEditFunction, data } = props;\n const { setRecords, reload } = data;\n const { router, messages, formRouteSegments } = useKitConfig();\n const navigate = router.useNavigate();\n const settings: ActionsSettings = { ...DEFAULT_ACTIONS_SETTINGS, ...props.actionsSettings };\n\n const deleteRecord = useMemo(\n () => getDeleteRecordFn(setRecords, service, reload, messages),\n [setRecords, service, reload, messages]\n );\n\n const actionsColumn: DataTableColumn<M> = {\n accessor: 'actions',\n title: <Box mr={6}>{messages.table.actions}</Box>,\n textAlign: 'right',\n render: item => (\n <RowActions\n {...settings}\n viewUrl={viewUrl(props, item, formRouteSegments)}\n editUrl={item.id ? `${basePath}/${formRouteSegments.edit}/${item.id}` : undefined}\n onEdit={customEditFunction && item.id ? () => customEditFunction(item.id) : undefined}\n onDelete={() => deleteRecord(item)}\n />\n )\n };\n const effectiveColumns = settings.canShowActions ? [...columns, actionsColumn] : columns;\n\n if (data.error) {\n return <ErrorAlert title={messages.common.loadError} message={String(data.error)} />;\n }\n\n return (\n <DataTableView\n records={data.records}\n state={data.tableState}\n paginated={data.paginated}\n columns={effectiveColumns}\n loading={data.loading}\n // Highlights the row under the mouse, to show that clicking it opens the record, as the view icon does.\n highlightOnHover\n highlightOnHoverColor={ROW_HOVER_COLOR}\n onRowClick={({ record }) => navigate(viewUrl(props, record, formRouteSegments))}\n />\n );\n};\n\nexport default Table;\n"],"mappings":";;;;;;;;;;AAuBA,IAAM,kBAAkB;;AAkBxB,IAAM,2BAA4C;CAC9C,gBAAgB;CAChB,SAAS;CACT,SAAS;CACT,WAAW;AACf;;;;;;;AAyCA,SAAS,QACL,OACA,MACA,UACM;CACN,OAAO,MAAM,aAAa,MAAM,WAAW,IAAI,IAAI,GAAG,MAAM,SAAS,GAAG,SAAS,KAAK,GAAG,KAAK;AAClG;;;;;;;;;AAUA,SAAS,kBACL,SACA,SACA,QACA,UAC0B;CAC1B,OAAO,OAAO,SAAY;EACtB,MAAM,KAAK,MAAM,MAAM;EAGvB,IAAI,CAAC,IAAI;GACL,SAAQ,aAAY,SAAS,QAAO,YAAW,YAAY,IAAI,CAAC;GAChE;EACJ;EAEA,IAAI;GACA,MAAM,QAAQ,OAAO,EAAE;GACvB,wBAAwB,SAAS,MAAM,eAAe,SAAS,OAAO,OAAO;GAC7E,MAAM,OAAO;EACjB,SAAS,GAAG;GACR,sBAAsB,GAAG,KAAA,GAAW,SAAS,OAAO,KAAK;EAC7D;CACJ;AACJ;;;;;;;;AASA,IAAM,SAA8D,UAA0C;CAC1G,MAAM,EAAE,SAAS,SAAS,UAAU,oBAAoB,SAAS;CACjE,MAAM,EAAE,YAAY,WAAW;CAC/B,MAAM,EAAE,QAAQ,UAAU,sBAAsB,aAAa;CAC7D,MAAM,WAAW,OAAO,YAAY;CACpC,MAAM,WAA4B;EAAE,GAAG;EAA0B,GAAG,MAAM;CAAgB;CAE1F,MAAM,eAAe,cACX,kBAAkB,YAAY,SAAS,QAAQ,QAAQ,GAC7D;EAAC;EAAY;EAAS;EAAQ;CAAQ,CAC1C;CAEA,MAAM,gBAAoC;EACtC,UAAU;EACV,OAAO,oBAAC,KAAD;GAAK,IAAI;GAAI,UAAA,SAAS,MAAM;EAAa,CAAA;EAChD,WAAW;EACX,SAAQ,SACJ,oBAAC,YAAD;GACI,GAAI;GACJ,SAAS,QAAQ,OAAO,MAAM,iBAAiB;GAC/C,SAAS,KAAK,KAAK,GAAG,SAAS,GAAG,kBAAkB,KAAK,GAAG,KAAK,OAAO,KAAA;GACxE,QAAQ,sBAAsB,KAAK,WAAW,mBAAmB,KAAK,EAAE,IAAI,KAAA;GAC5E,gBAAgB,aAAa,IAAI;EACpC,CAAA;CAET;CACA,MAAM,mBAAmB,SAAS,iBAAiB,CAAC,GAAG,SAAS,aAAa,IAAI;CAEjF,IAAI,KAAK,OACL,OAAO,oBAAC,YAAD;EAAY,OAAO,SAAS,OAAO;EAAW,SAAS,OAAO,KAAK,KAAK;CAAI,CAAA;CAGvF,OACI,oBAAC,eAAD;EACI,SAAS,KAAK;EACd,OAAO,KAAK;EACZ,WAAW,KAAK;EAChB,SAAS;EACT,SAAS,KAAK;EAEd,kBAAA;EACA,uBAAuB;EACvB,aAAa,EAAE,aAAa,SAAS,QAAQ,OAAO,QAAQ,iBAAiB,CAAC;CACjF,CAAA;AAET"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { TableState } from '../ViewTable/TableState';
|
|
2
|
+
/** Function that sets the records of a table, as the one of `useState`. */
|
|
3
|
+
export type SetRecords<M> = (value: ((prevState: M[]) => M[]) | M[]) => void;
|
|
4
|
+
/**
|
|
5
|
+
* Records of a {@link Table} and their pagination and sort, given by `useClientPagination` (sorted and paginated in the
|
|
6
|
+
* browser) or `useServerPagination` (sorted and paginated by the backend).
|
|
7
|
+
* @template M type of the records
|
|
8
|
+
*/
|
|
9
|
+
export type TableData<M> = {
|
|
10
|
+
/** Records shown, already sorted and cut to the current page. */
|
|
11
|
+
records: M[];
|
|
12
|
+
/** Changes the records of the table without fetching them (e.g. to remove one that was not saved yet). */
|
|
13
|
+
setRecords: SetRecords<M>;
|
|
14
|
+
/** Fetches the records again (e.g. after a deletion). */
|
|
15
|
+
reload: () => Promise<void>;
|
|
16
|
+
/** Whether the records are being fetched. */
|
|
17
|
+
loading: boolean;
|
|
18
|
+
/** Error of the last fetch; `null` if there is none. */
|
|
19
|
+
error: unknown;
|
|
20
|
+
/** Pagination and sort of the table. */
|
|
21
|
+
tableState: TableState<M>;
|
|
22
|
+
/** Whether the pagination controls are shown. */
|
|
23
|
+
paginated: boolean;
|
|
24
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { TableData } from './TableData';
|
|
2
|
+
import { ListDataSource } from '../../../data/ListDataSource';
|
|
3
|
+
import { Identifiable } from '../../../model/Identifiable';
|
|
4
|
+
/** Options of {@link useClientPagination}. */
|
|
5
|
+
export type ClientPaginationOptions = {
|
|
6
|
+
/** Whether the table is paginated; defaults to true. Without it, every record is shown. */
|
|
7
|
+
pagination?: boolean;
|
|
8
|
+
};
|
|
9
|
+
/** Fetches all records of a table. */
|
|
10
|
+
export type FetchAll<M> = () => Promise<M[]>;
|
|
11
|
+
/**
|
|
12
|
+
* Fetches all records of a {@link Table} at once (e.g. from an API), which the browser then sorts (following the locale)
|
|
13
|
+
* and paginates.
|
|
14
|
+
* @template M type of the records
|
|
15
|
+
* @param source an object whose `findAll` fetches all records (e.g. a service), or a function that fetches them (e.g.
|
|
16
|
+
* with a filter). It must be stable (e.g. a module constant, or a function created with `useCallback`), since a
|
|
17
|
+
* new object or function fetches again: a `new Service()` inside the component would fetch on every render
|
|
18
|
+
* @param options whether the table is paginated
|
|
19
|
+
* @return the data to give to the `data` prop of the {@link Table}
|
|
20
|
+
*/
|
|
21
|
+
export declare function useClientPagination<M extends Identifiable>(source: Pick<ListDataSource<M>, 'findAll'> | FetchAll<M>, { pagination }?: ClientPaginationOptions): TableData<M>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useClientTableState } from "../ViewTable/useClientTableState.js";
|
|
3
|
+
import useService from "../../../hook/useService.js";
|
|
4
|
+
import { useKitConfig } from "../../../provider/useKit.js";
|
|
5
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
6
|
+
// src/component/organism/Table/useClientPagination.ts
|
|
7
|
+
/** Initial records of the table, while they are fetched; a single constant, so it is stable across renders. */
|
|
8
|
+
var NO_RECORDS = [];
|
|
9
|
+
/**
|
|
10
|
+
* Fetches all records of a {@link Table} at once (e.g. from an API), which the browser then sorts (following the locale)
|
|
11
|
+
* and paginates.
|
|
12
|
+
* @template M type of the records
|
|
13
|
+
* @param source an object whose `findAll` fetches all records (e.g. a service), or a function that fetches them (e.g.
|
|
14
|
+
* with a filter). It must be stable (e.g. a module constant, or a function created with `useCallback`), since a
|
|
15
|
+
* new object or function fetches again: a `new Service()` inside the component would fetch on every render
|
|
16
|
+
* @param options whether the table is paginated
|
|
17
|
+
* @return the data to give to the `data` prop of the {@link Table}
|
|
18
|
+
*/
|
|
19
|
+
function useClientPagination(source, { pagination = true } = {}) {
|
|
20
|
+
const fetchAll = useMemo(() => typeof source === "function" ? source : () => source.findAll(), [source]);
|
|
21
|
+
const { locale } = useKitConfig();
|
|
22
|
+
const { data: fetchedData, loading, error } = useService(fetchAll, NO_RECORDS);
|
|
23
|
+
const [data, setData] = useState(NO_RECORDS);
|
|
24
|
+
useEffect(() => setData(fetchedData), [fetchedData]);
|
|
25
|
+
const reload = useCallback(async () => setData(await fetchAll()), [fetchAll]);
|
|
26
|
+
const { records, ...tableState } = useClientTableState({
|
|
27
|
+
data,
|
|
28
|
+
pagination,
|
|
29
|
+
locale
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
records,
|
|
33
|
+
setRecords: setData,
|
|
34
|
+
reload,
|
|
35
|
+
loading,
|
|
36
|
+
error,
|
|
37
|
+
tableState,
|
|
38
|
+
paginated: pagination
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export { useClientPagination };
|
|
42
|
+
|
|
43
|
+
//# sourceMappingURL=useClientPagination.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useClientPagination.js","names":[],"sources":["../../../../src/component/organism/Table/useClientPagination.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useState } from 'react';\n\nimport { TableData } from './TableData';\nimport { useClientTableState } from '../ViewTable/useClientTableState';\nimport { ListDataSource } from '../../../data/ListDataSource';\nimport useService from '../../../hook/useService';\nimport { Identifiable } from '../../../model/Identifiable';\nimport { useKitConfig } from '../../../provider/useKit';\n\n/** Initial records of the table, while they are fetched; a single constant, so it is stable across renders. */\nconst NO_RECORDS: never[] = [];\n\n/** Options of {@link useClientPagination}. */\nexport type ClientPaginationOptions = {\n /** Whether the table is paginated; defaults to true. Without it, every record is shown. */\n pagination?: boolean;\n};\n\n/** Fetches all records of a table. */\nexport type FetchAll<M> = () => Promise<M[]>;\n\n/**\n * Fetches all records of a {@link Table} at once (e.g. from an API), which the browser then sorts (following the locale)\n * and paginates.\n * @template M type of the records\n * @param source an object whose `findAll` fetches all records (e.g. a service), or a function that fetches them (e.g.\n * with a filter). It must be stable (e.g. a module constant, or a function created with `useCallback`), since a\n * new object or function fetches again: a `new Service()` inside the component would fetch on every render\n * @param options whether the table is paginated\n * @return the data to give to the `data` prop of the {@link Table}\n */\nexport function useClientPagination<M extends Identifiable>(\n source: Pick<ListDataSource<M>, 'findAll'> | FetchAll<M>,\n { pagination = true }: ClientPaginationOptions = {}\n): TableData<M> {\n // Calls the method on the object, so it keeps its `this`.\n const fetchAll = useMemo(() => (typeof source === 'function' ? source : () => source.findAll()), [source]);\n const { locale } = useKitConfig();\n const { data: fetchedData, loading, error } = useService<M[]>(fetchAll, NO_RECORDS);\n const [data, setData] = useState<M[]>(NO_RECORDS);\n\n useEffect(() => setData(fetchedData), [fetchedData]);\n\n const reload = useCallback(async () => setData(await fetchAll()), [fetchAll]);\n const { records, ...tableState } = useClientTableState({ data, pagination, locale });\n\n return { records, setRecords: setData, reload, loading, error, tableState, paginated: pagination };\n}\n"],"mappings":";;;;;;;AAUA,IAAM,aAAsB,CAAC;;;;;;;;;;;AAqB7B,SAAgB,oBACZ,QACA,EAAE,aAAa,SAAkC,CAAC,GACtC;CAEZ,MAAM,WAAW,cAAe,OAAO,WAAW,aAAa,eAAe,OAAO,QAAQ,GAAI,CAAC,MAAM,CAAC;CACzG,MAAM,EAAE,WAAW,aAAa;CAChC,MAAM,EAAE,MAAM,aAAa,SAAS,UAAU,WAAgB,UAAU,UAAU;CAClF,MAAM,CAAC,MAAM,WAAW,SAAc,UAAU;CAEhD,gBAAgB,QAAQ,WAAW,GAAG,CAAC,WAAW,CAAC;CAEnD,MAAM,SAAS,YAAY,YAAY,QAAQ,MAAM,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC;CAC5E,MAAM,EAAE,SAAS,GAAG,eAAe,oBAAoB;EAAE;EAAM;EAAY;CAAO,CAAC;CAEnF,OAAO;EAAE;EAAS,YAAY;EAAS;EAAQ;EAAS;EAAO;EAAY,WAAW;CAAW;AACrG"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { TableData } from './TableData';
|
|
2
|
+
import { PageDataSource } from '../../../data/PageDataSource';
|
|
3
|
+
import { FetchPage } from '../../../data/PageRequest';
|
|
4
|
+
/** Options of {@link useServerPagination}. */
|
|
5
|
+
export type ServerPaginationOptions = {
|
|
6
|
+
/**
|
|
7
|
+
* Number of the first page in the requests to the backend (e.g. 0 for Spring Data); defaults to 1. It only changes
|
|
8
|
+
* the `page` sent to the backend: the table always shows the pages starting at 1.
|
|
9
|
+
*/
|
|
10
|
+
firstPage?: number;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Fetches one page at a time of a {@link Table}, already sorted and paginated by the backend. It goes back to page 1
|
|
14
|
+
* when the page size, the sort or the filter changes, and to the last existing page when the current one no longer
|
|
15
|
+
* exists (e.g. its last record was deleted).
|
|
16
|
+
* @template M type of the records
|
|
17
|
+
* @param source an object whose `findPage` fetches a page (e.g. a service), or a function that fetches it (e.g. with a
|
|
18
|
+
* filter). A new object or function means that the filter changed, so it goes back to page 1 and fetches again.
|
|
19
|
+
* Keep it stable otherwise (e.g. a module constant, or a function created with `useCallback`): a
|
|
20
|
+
* `new Service()` inside the component would fetch on every render
|
|
21
|
+
* @param options number of the first page for the backend
|
|
22
|
+
* @return the data to give to the `data` prop of the {@link Table}
|
|
23
|
+
*/
|
|
24
|
+
export declare function useServerPagination<M>(source: PageDataSource<M> | FetchPage<M>, { firstPage }?: ServerPaginationOptions): TableData<M>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { DEFAULT_SORT_STATUS, PAGE_SIZES } from "../ViewTable/TableState.js";
|
|
3
|
+
import useService from "../../../hook/useService.js";
|
|
4
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
5
|
+
// src/component/organism/Table/useServerPagination.ts
|
|
6
|
+
/** Result before the first response; a single constant, so it is stable across renders. */
|
|
7
|
+
var NO_PAGE = {
|
|
8
|
+
records: [],
|
|
9
|
+
totalRecords: 0
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Builds what is sent to the backend. The table state keeps the `columnAccessor`, as the header needs it to show the
|
|
13
|
+
* sort indicator, but the backend must receive the `sortKey`, when the column has one.
|
|
14
|
+
* @param request page (starting at {@link FIRST_PAGE}), size and sort chosen in the table
|
|
15
|
+
* @param firstPage number of the first page for the backend
|
|
16
|
+
*/
|
|
17
|
+
function toBackendRequest(request, firstPage) {
|
|
18
|
+
const { page, sort } = request;
|
|
19
|
+
return {
|
|
20
|
+
...request,
|
|
21
|
+
page: page - 1 + firstPage,
|
|
22
|
+
sort: {
|
|
23
|
+
...sort,
|
|
24
|
+
columnAccessor: sort.sortKey ?? sort.columnAccessor
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Fetches one page at a time of a {@link Table}, already sorted and paginated by the backend. It goes back to page 1
|
|
30
|
+
* when the page size, the sort or the filter changes, and to the last existing page when the current one no longer
|
|
31
|
+
* exists (e.g. its last record was deleted).
|
|
32
|
+
* @template M type of the records
|
|
33
|
+
* @param source an object whose `findPage` fetches a page (e.g. a service), or a function that fetches it (e.g. with a
|
|
34
|
+
* filter). A new object or function means that the filter changed, so it goes back to page 1 and fetches again.
|
|
35
|
+
* Keep it stable otherwise (e.g. a module constant, or a function created with `useCallback`): a
|
|
36
|
+
* `new Service()` inside the component would fetch on every render
|
|
37
|
+
* @param options number of the first page for the backend
|
|
38
|
+
* @return the data to give to the `data` prop of the {@link Table}
|
|
39
|
+
*/
|
|
40
|
+
function useServerPagination(source, { firstPage = 1 } = {}) {
|
|
41
|
+
const fetchPage = useMemo(() => typeof source === "function" ? source : (request) => source.findPage(request), [source]);
|
|
42
|
+
const [request, setRequest] = useState({
|
|
43
|
+
page: 1,
|
|
44
|
+
size: PAGE_SIZES[0],
|
|
45
|
+
sort: DEFAULT_SORT_STATUS
|
|
46
|
+
});
|
|
47
|
+
const [previousFetchPage, setPreviousFetchPage] = useState(() => fetchPage);
|
|
48
|
+
if (fetchPage !== previousFetchPage) {
|
|
49
|
+
setPreviousFetchPage(() => fetchPage);
|
|
50
|
+
setRequest((current) => ({
|
|
51
|
+
...current,
|
|
52
|
+
page: 1
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
const fetchCurrentPage = useCallback(() => fetchPage(toBackendRequest(request, firstPage)), [
|
|
56
|
+
fetchPage,
|
|
57
|
+
request,
|
|
58
|
+
firstPage
|
|
59
|
+
]);
|
|
60
|
+
const { data, loading, error } = useService(fetchCurrentPage, NO_PAGE);
|
|
61
|
+
const [records, setRecords] = useState([]);
|
|
62
|
+
useEffect(() => setRecords(data.records), [data]);
|
|
63
|
+
const lastPage = Math.max(Math.ceil(data.totalRecords / request.size), 1);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (!loading && request.page > lastPage) setRequest((current) => ({
|
|
66
|
+
...current,
|
|
67
|
+
page: lastPage
|
|
68
|
+
}));
|
|
69
|
+
}, [
|
|
70
|
+
loading,
|
|
71
|
+
request.page,
|
|
72
|
+
lastPage
|
|
73
|
+
]);
|
|
74
|
+
return {
|
|
75
|
+
records,
|
|
76
|
+
setRecords,
|
|
77
|
+
reload: useCallback(async () => setRequest((current) => ({ ...current })), []),
|
|
78
|
+
loading,
|
|
79
|
+
error,
|
|
80
|
+
tableState: {
|
|
81
|
+
page: request.page,
|
|
82
|
+
pageSize: request.size,
|
|
83
|
+
sortStatus: request.sort,
|
|
84
|
+
totalRecords: data.totalRecords,
|
|
85
|
+
onPageChange: (page) => setRequest((current) => ({
|
|
86
|
+
...current,
|
|
87
|
+
page
|
|
88
|
+
})),
|
|
89
|
+
onPageSizeChange: (size) => setRequest((current) => ({
|
|
90
|
+
...current,
|
|
91
|
+
size,
|
|
92
|
+
page: 1
|
|
93
|
+
})),
|
|
94
|
+
onSortStatusChange: (sort) => setRequest((current) => ({
|
|
95
|
+
...current,
|
|
96
|
+
sort,
|
|
97
|
+
page: 1
|
|
98
|
+
}))
|
|
99
|
+
},
|
|
100
|
+
paginated: true
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export { useServerPagination };
|
|
104
|
+
|
|
105
|
+
//# sourceMappingURL=useServerPagination.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useServerPagination.js","names":[],"sources":["../../../../src/component/organism/Table/useServerPagination.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useState } from 'react';\n\nimport { TableData } from './TableData';\nimport { DEFAULT_SORT_STATUS, FIRST_PAGE, PAGE_SIZES } from '../ViewTable/TableState';\nimport { PageDataSource } from '../../../data/PageDataSource';\nimport { FetchPage, PageRequest, PageResult } from '../../../data/PageRequest';\nimport useService from '../../../hook/useService';\n\n/** Result before the first response; a single constant, so it is stable across renders. */\nconst NO_PAGE: PageResult<never> = { records: [], totalRecords: 0 };\n\n/** Options of {@link useServerPagination}. */\nexport type ServerPaginationOptions = {\n /**\n * Number of the first page in the requests to the backend (e.g. 0 for Spring Data); defaults to 1. It only changes\n * the `page` sent to the backend: the table always shows the pages starting at 1.\n */\n firstPage?: number;\n};\n\n/**\n * Builds what is sent to the backend. The table state keeps the `columnAccessor`, as the header needs it to show the\n * sort indicator, but the backend must receive the `sortKey`, when the column has one.\n * @param request page (starting at {@link FIRST_PAGE}), size and sort chosen in the table\n * @param firstPage number of the first page for the backend\n */\nfunction toBackendRequest<M>(request: PageRequest<M>, firstPage: number): PageRequest<M> {\n const { page, sort } = request;\n\n return {\n ...request,\n page: page - FIRST_PAGE + firstPage,\n sort: { ...sort, columnAccessor: sort.sortKey ?? sort.columnAccessor }\n };\n}\n\n/**\n * Fetches one page at a time of a {@link Table}, already sorted and paginated by the backend. It goes back to page 1\n * when the page size, the sort or the filter changes, and to the last existing page when the current one no longer\n * exists (e.g. its last record was deleted).\n * @template M type of the records\n * @param source an object whose `findPage` fetches a page (e.g. a service), or a function that fetches it (e.g. with a\n * filter). A new object or function means that the filter changed, so it goes back to page 1 and fetches again.\n * Keep it stable otherwise (e.g. a module constant, or a function created with `useCallback`): a\n * `new Service()` inside the component would fetch on every render\n * @param options number of the first page for the backend\n * @return the data to give to the `data` prop of the {@link Table}\n */\nexport function useServerPagination<M>(\n source: PageDataSource<M> | FetchPage<M>,\n { firstPage = FIRST_PAGE }: ServerPaginationOptions = {}\n): TableData<M> {\n // Calls the method on the object, so it keeps its `this`.\n const fetchPage = useMemo<FetchPage<M>>(\n () => (typeof source === 'function' ? source : request => source.findPage(request)),\n [source]\n );\n // A single object, so a copy of it fetches the same page again, and a change is always a single fetch.\n const [request, setRequest] = useState<PageRequest<M>>({\n page: FIRST_PAGE,\n size: PAGE_SIZES[0],\n sort: DEFAULT_SORT_STATUS\n });\n const [previousFetchPage, setPreviousFetchPage] = useState(() => fetchPage);\n\n /* Done while rendering, as an effect would first fetch the new filter with the old page.\n See https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes */\n if (fetchPage !== previousFetchPage) {\n setPreviousFetchPage(() => fetchPage);\n setRequest(current => ({ ...current, page: FIRST_PAGE }));\n }\n\n const fetchCurrentPage = useCallback(\n () => fetchPage(toBackendRequest(request, firstPage)),\n [fetchPage, request, firstPage]\n );\n const { data, loading, error } = useService<PageResult<M>>(fetchCurrentPage, NO_PAGE);\n const [records, setRecords] = useState<M[]>([]);\n\n useEffect(() => setRecords(data.records), [data]);\n\n const lastPage = Math.max(Math.ceil(data.totalRecords / request.size), FIRST_PAGE);\n\n // The current page may not exist anymore (e.g. its last record was deleted).\n useEffect(() => {\n if (!loading && request.page > lastPage) setRequest(current => ({ ...current, page: lastPage }));\n }, [loading, request.page, lastPage]);\n\n const reload = useCallback(async () => setRequest(current => ({ ...current })), []);\n\n return {\n records,\n setRecords,\n reload,\n loading,\n error,\n tableState: {\n page: request.page,\n pageSize: request.size,\n sortStatus: request.sort,\n totalRecords: data.totalRecords,\n onPageChange: page => setRequest(current => ({ ...current, page })),\n onPageSizeChange: size => setRequest(current => ({ ...current, size, page: FIRST_PAGE })),\n onSortStatusChange: sort => setRequest(current => ({ ...current, sort, page: FIRST_PAGE }))\n },\n paginated: true\n };\n}\n"],"mappings":";;;;;;AASA,IAAM,UAA6B;CAAE,SAAS,CAAC;CAAG,cAAc;AAAE;;;;;;;AAiBlE,SAAS,iBAAoB,SAAyB,WAAmC;CACrF,MAAM,EAAE,MAAM,SAAS;CAEvB,OAAO;EACH,GAAG;EACH,MAAM,OAAA,IAAoB;EAC1B,MAAM;GAAE,GAAG;GAAM,gBAAgB,KAAK,WAAW,KAAK;EAAe;CACzE;AACJ;;;;;;;;;;;;;AAcA,SAAgB,oBACZ,QACA,EAAE,YAAA,MAAoD,CAAC,GAC3C;CAEZ,MAAM,YAAY,cACP,OAAO,WAAW,aAAa,UAAS,YAAW,OAAO,SAAS,OAAO,GACjF,CAAC,MAAM,CACX;CAEA,MAAM,CAAC,SAAS,cAAc,SAAyB;EACnD,MAAA;EACA,MAAM,WAAW;EACjB,MAAM;CACV,CAAC;CACD,MAAM,CAAC,mBAAmB,wBAAwB,eAAe,SAAS;CAI1E,IAAI,cAAc,mBAAmB;EACjC,2BAA2B,SAAS;EACpC,YAAW,aAAY;GAAE,GAAG;GAAS,MAAA;EAAiB,EAAE;CAC5D;CAEA,MAAM,mBAAmB,kBACf,UAAU,iBAAiB,SAAS,SAAS,CAAC,GACpD;EAAC;EAAW;EAAS;CAAS,CAClC;CACA,MAAM,EAAE,MAAM,SAAS,UAAU,WAA0B,kBAAkB,OAAO;CACpF,MAAM,CAAC,SAAS,cAAc,SAAc,CAAC,CAAC;CAE9C,gBAAgB,WAAW,KAAK,OAAO,GAAG,CAAC,IAAI,CAAC;CAEhD,MAAM,WAAW,KAAK,IAAI,KAAK,KAAK,KAAK,eAAe,QAAQ,IAAI,GAAA,CAAa;CAGjF,gBAAgB;EACZ,IAAI,CAAC,WAAW,QAAQ,OAAO,UAAU,YAAW,aAAY;GAAE,GAAG;GAAS,MAAM;EAAS,EAAE;CACnG,GAAG;EAAC;EAAS,QAAQ;EAAM;CAAQ,CAAC;CAIpC,OAAO;EACH;EACA;EACA,QALW,YAAY,YAAY,YAAW,aAAY,EAAE,GAAG,QAAQ,EAAE,GAAG,CAAC,CAK7E;EACA;EACA;EACA,YAAY;GACR,MAAM,QAAQ;GACd,UAAU,QAAQ;GAClB,YAAY,QAAQ;GACpB,cAAc,KAAK;GACnB,eAAc,SAAQ,YAAW,aAAY;IAAE,GAAG;IAAS;GAAK,EAAE;GAClE,mBAAkB,SAAQ,YAAW,aAAY;IAAE,GAAG;IAAS;IAAM,MAAA;GAAiB,EAAE;GACxF,qBAAoB,SAAQ,YAAW,aAAY;IAAE,GAAG;IAAS;IAAM,MAAA;GAAiB,EAAE;EAC9F;EACA,WAAW;CACf;AACJ"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { DataTableColumn, DataTablePaginationProps, DataTableProps } from 'mantine-datatable';
|
|
2
|
+
import { Identifiable } from '../../../model/Identifiable';
|
|
3
|
+
import { TableState } from './TableState';
|
|
4
|
+
/** Function that defines extra attributes (e.g. aria-label) for each pagination control button. */
|
|
5
|
+
type PaginationControlPropsGetter = Extract<DataTablePaginationProps, {
|
|
6
|
+
page: number;
|
|
7
|
+
}>['getPaginationControlProps'];
|
|
8
|
+
/**
|
|
9
|
+
* Props of the {@link DataTable} passed straight through.
|
|
10
|
+
* Only the props actually used by the callers are accepted: using `Partial<DataTableProps<M>>` makes TypeScript combine
|
|
11
|
+
* the variants of the `DataTableProps` union with the ones of `paginationProps`, which exhausts the memory of `tsc`.
|
|
12
|
+
*/
|
|
13
|
+
type DataTablePassThroughProps<M extends Identifiable> = Pick<DataTableProps<M>, 'onRowClick' | 'rowStyle' | 'noRecordsText' | 'highlightOnHover' | 'highlightOnHoverColor' | 'className' | 'minHeight' | 'withColumnBorders' | 'verticalSpacing' | 'horizontalSpacing'>;
|
|
14
|
+
/** Props of the {@link DataTableView} that do not depend on where the table is sorted and paginated. */
|
|
15
|
+
export type DataTableViewBaseProps<M extends Identifiable> = {
|
|
16
|
+
columns: DataTableColumn<M>[];
|
|
17
|
+
/** Whether the records are being loaded. */
|
|
18
|
+
loading?: boolean;
|
|
19
|
+
/** Shows the ID column, automatically added to the given columns. */
|
|
20
|
+
showIdColumn?: boolean;
|
|
21
|
+
/** Extra attributes for the pagination control buttons, when the table is paginated. */
|
|
22
|
+
getPaginationControlProps?: PaginationControlPropsGetter;
|
|
23
|
+
} & DataTablePassThroughProps<M>;
|
|
24
|
+
/** Props of the {@link DataTableView}. */
|
|
25
|
+
type DataTableViewProps<M extends Identifiable> = DataTableViewBaseProps<M> & {
|
|
26
|
+
/** Records shown, already sorted and cut to the current page. */
|
|
27
|
+
records: M[];
|
|
28
|
+
/** Pagination and sort of the table. */
|
|
29
|
+
state: TableState<M>;
|
|
30
|
+
/** Whether the pagination controls are shown. */
|
|
31
|
+
paginated: boolean;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Shows records as received, with the given pagination and sort (e.g. kept by a backend that paginates). It is the
|
|
35
|
+
* visual part of the tables, on top of the mantine-datatable `DataTable`, with the texts of the locale.
|
|
36
|
+
* @template M type of the records, shown one per row
|
|
37
|
+
*/
|
|
38
|
+
declare const DataTableView: <M extends Identifiable>({ records, state, paginated, columns, loading, showIdColumn, getPaginationControlProps, ...rest }: DataTableViewProps<M>) => import("react/jsx-runtime").JSX.Element;
|
|
39
|
+
export default DataTableView;
|