@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.
Files changed (38) hide show
  1. package/README.md +81 -10
  2. package/dist/component/organism/Table/Table.d.ts +9 -9
  3. package/dist/component/organism/Table/Table.js +21 -24
  4. package/dist/component/organism/Table/Table.js.map +1 -1
  5. package/dist/component/organism/Table/TableData.d.ts +24 -0
  6. package/dist/component/organism/Table/TableData.js +1 -0
  7. package/dist/component/organism/Table/useClientPagination.d.ts +21 -0
  8. package/dist/component/organism/Table/useClientPagination.js +43 -0
  9. package/dist/component/organism/Table/useClientPagination.js.map +1 -0
  10. package/dist/component/organism/Table/useServerPagination.d.ts +24 -0
  11. package/dist/component/organism/Table/useServerPagination.js +105 -0
  12. package/dist/component/organism/Table/useServerPagination.js.map +1 -0
  13. package/dist/component/organism/ViewTable/DataTableView.d.ts +39 -0
  14. package/dist/component/organism/ViewTable/DataTableView.js +52 -0
  15. package/dist/component/organism/ViewTable/DataTableView.js.map +1 -0
  16. package/dist/component/organism/ViewTable/TableState.d.ts +30 -0
  17. package/dist/component/organism/ViewTable/TableState.js +19 -0
  18. package/dist/component/organism/ViewTable/TableState.js.map +1 -0
  19. package/dist/component/organism/ViewTable/ViewTable.d.ts +11 -33
  20. package/dist/component/organism/ViewTable/ViewTable.js +16 -101
  21. package/dist/component/organism/ViewTable/ViewTable.js.map +1 -1
  22. package/dist/component/organism/ViewTable/useClientTableState.d.ts +25 -0
  23. package/dist/component/organism/ViewTable/useClientTableState.js +68 -0
  24. package/dist/component/organism/ViewTable/useClientTableState.js.map +1 -0
  25. package/dist/component/template/ListPage/ListPage.d.ts +9 -19
  26. package/dist/component/template/ListPage/ListPage.js +5 -14
  27. package/dist/component/template/ListPage/ListPage.js.map +1 -1
  28. package/dist/data/PageDataSource.d.ts +10 -0
  29. package/dist/data/PageDataSource.js +1 -0
  30. package/dist/data/PageRequest.d.ts +29 -0
  31. package/dist/data/PageRequest.js +1 -0
  32. package/dist/hook/useService.d.ts +3 -3
  33. package/dist/hook/useService.js +2 -0
  34. package/dist/hook/useService.js.map +1 -1
  35. package/dist/util/ArrayUtil.d.ts +4 -3
  36. package/dist/util/ArrayUtil.js +3 -2
  37. package/dist/util/ArrayUtil.js.map +1 -1
  38. 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`, `ListPage` |
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` shows the records in a sortable, paginated table with view, edit and delete actions. Its `service` is any
121
- object with `findAll` and `delete`:
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
- service={regions}
142
- resourceBasePath="/region"
143
- tableColumns={[{ accessor: 'name', title: 'Name', sortable: true }]}
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 the records of a data source, with view, edit and delete (with confirmation) actions. |
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` | Sortable and paginated table (on top of mantine-datatable), with locale-aware sorting. |
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 `Table`. |
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
- * Fetches the records shown, instead of `service.findAll()`. To show data already available, use
22
- * `() => Promise.resolve(data)`.
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
- customFetchMethod?: () => Promise<M[]>;
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. Clicking a row opens the
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 ViewTable from "../ViewTable/ViewTable.js";
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 { useCallback, useEffect, useMemo, useState } from "react";
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, obtained from `useState`
30
+ * @param setData function that sets the records of the table
34
31
  * @param service source of the records, which deletes them
35
- * @param fetchMethod fetches the records again after the deletion
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, fetchMethod, messages) {
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
- setData(await fetchMethod());
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. Clicking a row opens the
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 { customFetchMethod, service, columns, basePath, customEditFunction } = props;
63
- const fetchMethod = useCallback(() => customFetchMethod ? customFetchMethod() : service.findAll(), [customFetchMethod, service]);
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 { data: fetchedData, loading, error } = useService(fetchMethod, NO_RECORDS);
71
- const [data, setData] = useState([]);
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
- fetchMethod,
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(ViewTable, {
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, useCallback, useEffect, useMemo, useState } from 'react';\n\nimport { Box } from '@mantine/core';\nimport { DataTableColumn } from 'mantine-datatable';\n\nimport ViewTable from '../ViewTable/ViewTable';\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 useService from '../../../hook/useService';\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/** Initial records of the table, while they are fetched; a single constant, so it is stable across renders. */\nconst NO_RECORDS: readonly Identifiable[] = [];\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 /** Source of the records and of their deletion (e.g. a service that accesses an API). */\n service: S;\n\n /**\n * Fetches the records shown, instead of `service.findAll()`. To show data already available, use\n * `() => Promise.resolve(data)`.\n */\n customFetchMethod?: () => Promise<M[]>;\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 /** Whether the table is paginated. */\n pagination?: boolean;\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, obtained from `useState`\n * @param service source of the records, which deletes them\n * @param fetchMethod 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: (value: ((prevState: M[]) => M[]) | M[]) => void,\n service: S,\n fetchMethod: () => Promise<M[]>,\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 setData(await fetchMethod());\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. Clicking a row opens the\n * record.\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 { customFetchMethod, service, columns, basePath, customEditFunction } = props;\n const fetchMethod = useCallback(\n () => (customFetchMethod ? customFetchMethod() : service.findAll()),\n [customFetchMethod, service]\n );\n const { router, messages, formRouteSegments } = useKitConfig();\n const navigate = router.useNavigate();\n const settings: ActionsSettings = { ...DEFAULT_ACTIONS_SETTINGS, ...props.actionsSettings };\n\n const { data: fetchedData, loading, error } = useService(fetchMethod, NO_RECORDS as M[]);\n const [data, setData] = useState<M[]>([]);\n\n useEffect(() => setData(fetchedData), [fetchedData]);\n\n const deleteRecord = useMemo(\n () => getDeleteRecordFn(setData, service, fetchMethod, messages),\n [service, fetchMethod, 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 (error) {\n return <ErrorAlert title={messages.common.loadError} message={String(error)} />;\n }\n\n return (\n <ViewTable\n data={data}\n columns={effectiveColumns}\n loading={loading}\n pagination={props.pagination}\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;;AAGxB,IAAM,aAAsC,CAAC;;AAkB7C,IAAM,2BAA4C;CAC9C,gBAAgB;CAChB,SAAS;CACT,SAAS;CACT,WAAW;AACf;;;;;;;AA4CA,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,aACA,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,QAAQ,MAAM,YAAY,CAAC;EAC/B,SAAS,GAAG;GACR,sBAAsB,GAAG,KAAA,GAAW,SAAS,OAAO,KAAK;EAC7D;CACJ;AACJ;;;;;;;AAQA,IAAM,SAA8D,UAA0C;CAC1G,MAAM,EAAE,mBAAmB,SAAS,SAAS,UAAU,uBAAuB;CAC9E,MAAM,cAAc,kBACT,oBAAoB,kBAAkB,IAAI,QAAQ,QAAQ,GACjE,CAAC,mBAAmB,OAAO,CAC/B;CACA,MAAM,EAAE,QAAQ,UAAU,sBAAsB,aAAa;CAC7D,MAAM,WAAW,OAAO,YAAY;CACpC,MAAM,WAA4B;EAAE,GAAG;EAA0B,GAAG,MAAM;CAAgB;CAE1F,MAAM,EAAE,MAAM,aAAa,SAAS,UAAU,WAAW,aAAa,UAAiB;CACvF,MAAM,CAAC,MAAM,WAAW,SAAc,CAAC,CAAC;CAExC,gBAAgB,QAAQ,WAAW,GAAG,CAAC,WAAW,CAAC;CAEnD,MAAM,eAAe,cACX,kBAAkB,SAAS,SAAS,aAAa,QAAQ,GAC/D;EAAC;EAAS;EAAa;CAAQ,CACnC;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,OACA,OAAO,oBAAC,YAAD;EAAY,OAAO,SAAS,OAAO;EAAW,SAAS,OAAO,KAAK;CAAI,CAAA;CAGlF,OACI,oBAAC,WAAD;EACU;EACN,SAAS;EACA;EACT,YAAY,MAAM;EAElB,kBAAA;EACA,uBAAuB;EACvB,aAAa,EAAE,aAAa,SAAS,QAAQ,OAAO,QAAQ,iBAAiB,CAAC;CACjF,CAAA;AAET"}
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;