innoboxrr-react-datatable 2.2.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -51
- package/index.js +5 -4
- package/package.json +6 -3
- package/src/DataTable.jsx +165 -96
- package/src/components/DataTableComponent.jsx +150 -89
- package/src/components/SelectPaginationComponent.jsx +56 -58
- package/src/table.js +251 -0
- package/src/useDataTable.js +310 -168
- package/src/useTheme.js +17 -0
- package/src/components/ActionListComponent.jsx +0 -62
- package/src/components/DatatableIcon.jsx +0 -39
- package/src/components/DisabledLinkComponent.jsx +0 -23
- package/src/components/IconLinkComponent.jsx +0 -25
- package/src/components/IconRouteComponent.jsx +0 -31
- package/src/components/NavDropdownComponent.jsx +0 -35
package/README.md
CHANGED
|
@@ -1,87 +1,103 @@
|
|
|
1
1
|
# innoboxrr-react-datatable
|
|
2
2
|
|
|
3
|
-
Gemelo React de [`innoboxrr-vue-datatable`](../vue-datatable)
|
|
4
|
-
el mismo contrato de modelo
|
|
3
|
+
Gemelo React de [`innoboxrr-vue-datatable`](../vue-datatable): los mismos
|
|
4
|
+
props, el mismo contrato de modelo y el mismo comportamiento —barra de
|
|
5
|
+
acciones, filtros, orden y paginación en el servidor, selección con acciones
|
|
6
|
+
masivas, permisos antes de abrir cada menú, esqueletos y errores que se ven—.
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
Por debajo, [TanStack Table](https://tanstack.com/table) lleva el estado de la
|
|
9
|
+
tabla y `innoboxrr-react-form-elements` pone el menú, los iconos y los
|
|
10
|
+
esqueletos, con el tema de `innoboxrr-form-core`.
|
|
9
11
|
|
|
10
12
|
## Instalación
|
|
11
13
|
|
|
12
14
|
```
|
|
13
|
-
npm i innoboxrr-react-datatable
|
|
15
|
+
npm i innoboxrr-react-datatable innoboxrr-form-core innoboxrr-react-form-elements react-router-dom
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
import 'innoboxrr-form-core/styles'
|
|
14
20
|
```
|
|
15
21
|
|
|
16
22
|
## Uso
|
|
17
23
|
|
|
18
24
|
```jsx
|
|
25
|
+
import { useRef, useState } from 'react'
|
|
19
26
|
import DataTable, { registerRoutes } from 'innoboxrr-react-datatable'
|
|
20
|
-
import * as
|
|
27
|
+
import * as productModel from './models/product'
|
|
21
28
|
|
|
22
29
|
registerRoutes({
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
AdminShowPost: '/admin/posts/:id',
|
|
30
|
+
AdminCreateProduct: '/admin/products/create',
|
|
31
|
+
AdminEditProduct: '/admin/products/:id/edit',
|
|
26
32
|
})
|
|
27
33
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
export default function Products() {
|
|
35
|
+
const table = useRef(null)
|
|
36
|
+
const [filters, setFilters] = useState({})
|
|
37
|
+
|
|
38
|
+
// Tras crear o editar en un drawer: table.current.refresh()
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<DataTable
|
|
42
|
+
ref={table}
|
|
43
|
+
dataUrl={route('api.acme.shop.product.index')}
|
|
44
|
+
dataMethod="get"
|
|
45
|
+
policyUrl={route('api.acme.shop.product.policies')}
|
|
46
|
+
policyMethod="get"
|
|
47
|
+
model={productModel}
|
|
48
|
+
formFilters={filters}
|
|
49
|
+
selectable
|
|
50
|
+
filterForm={<FilterForm onSubmit={setFilters} />} />
|
|
51
|
+
)
|
|
52
|
+
}
|
|
33
53
|
```
|
|
34
54
|
|
|
55
|
+
Los avisos y las confirmaciones necesitan, una vez en la aplicación,
|
|
56
|
+
`ToastRegionComponent` y `ConfirmHostComponent` de
|
|
57
|
+
`innoboxrr-react-form-elements`.
|
|
58
|
+
|
|
59
|
+
Los props, el contrato del modelo, `bulkActions`, los permisos y lo que se ve
|
|
60
|
+
mientras carga o cuando falla están descritos en el
|
|
61
|
+
[README de la versión Vue](../vue-datatable/README.md): son idénticos.
|
|
62
|
+
|
|
35
63
|
## Equivalencias con la versión Vue
|
|
36
64
|
|
|
37
65
|
| Vue | React |
|
|
38
66
|
|---|---|
|
|
39
|
-
| `<
|
|
40
|
-
|
|
|
41
|
-
|
|
|
42
|
-
|
|
|
43
|
-
| `uk-toggle` sobre el formulario de filtros | estado del componente |
|
|
67
|
+
| `<template #filterForm>` | prop `filterForm` |
|
|
68
|
+
| `ref` con `defineExpose` | `ref` (React 19): `refresh()`, `clearSelection()`, `selectedIds`, `table` |
|
|
69
|
+
| rutas con nombre de vue-router | `registerRoutes()` (abajo) |
|
|
70
|
+
| componente de celda con `@callback` | componente de celda con `onCallback` |
|
|
44
71
|
|
|
45
|
-
|
|
46
|
-
`
|
|
47
|
-
`extraParams`, `extraQuery`, `hideColumns`, `cardWrapper`, `showTableHeader`—
|
|
48
|
-
se llama y significa lo mismo.
|
|
72
|
+
La lógica común —columnas, petición, errores, acciones— vive en
|
|
73
|
+
`src/table.js`, que es el mismo archivo en los dos paquetes.
|
|
49
74
|
|
|
50
75
|
## Rutas con nombre
|
|
51
76
|
|
|
52
|
-
El contrato
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
vue-router resuelve eso de fábrica; React Router 7 no tiene rutas con nombre.
|
|
59
|
-
`registerRoutes({ nombre: patrón })` cierra ese hueco, y el módulo generado lo
|
|
60
|
-
llama al montarse. Una ruta sin registrar **lanza**: devolver `#` escondería el
|
|
61
|
-
fallo hasta que alguien hiciera clic.
|
|
77
|
+
El contrato apunta a rutas **por nombre** (`params.to.name`). React Router 7 no
|
|
78
|
+
las tiene, así que `registerRoutes({ nombre: patrón })` dice a qué patrón
|
|
79
|
+
corresponde cada una. Una acción hacia una ruta sin registrar avisa al usuario y
|
|
80
|
+
deja el detalle en la consola.
|
|
62
81
|
|
|
63
82
|
## `useDataTable`
|
|
64
83
|
|
|
65
|
-
Toda la lógica —cargar, ordenar, paginar, resolver políticas— está en el hook,
|
|
66
|
-
fuera del componente. Se puede probar sin montar nada y sirve para pintar la
|
|
67
|
-
misma tabla de otra forma:
|
|
68
|
-
|
|
69
84
|
```jsx
|
|
70
|
-
|
|
85
|
+
import { useDataTable } from 'innoboxrr-react-datatable'
|
|
86
|
+
|
|
87
|
+
const { table, rows, meta, loading, error, sortColumn, updatePage, refresh } = useDataTable({ ...props, navigate, labels })
|
|
71
88
|
```
|
|
72
89
|
|
|
73
|
-
##
|
|
74
|
-
|
|
75
|
-
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
-
|
|
83
|
-
|
|
84
|
-
hacen lo mismo: la de dentro soporta `action.link` y la de fuera no.
|
|
90
|
+
## De 2.x a 3.0
|
|
91
|
+
|
|
92
|
+
- Ya no existen `ActionListComponent`, `DatatableIcon`, `NavDropdownComponent`,
|
|
93
|
+
`IconRouteComponent`, `IconLinkComponent` ni `DisabledLinkComponent`.
|
|
94
|
+
- `DataTableComponent` recibe la instancia de TanStack Table en `table`;
|
|
95
|
+
`SelectPaginationComponent` sigue recibiendo `meta` y `onPageChange`.
|
|
96
|
+
- Las acciones de ruta navegan desde el menú con `useNavigate`.
|
|
97
|
+
- Los textos están en español y se cambian con `labels`.
|
|
98
|
+
- Un fallo ya no se reintenta: se ve, y se reintenta a mano.
|
|
99
|
+
- `innoboxrr-react-form-elements` pasa a ser dependencia par, y
|
|
100
|
+
`innoboxrr-form-core` sube a `^2.6`.
|
|
85
101
|
|
|
86
102
|
## Pruebas
|
|
87
103
|
|
package/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Gemelo React de innoboxrr-vue-datatable.
|
|
3
3
|
*
|
|
4
|
-
* El export por defecto es la tabla, igual que en la
|
|
5
|
-
*
|
|
6
|
-
* y el registro de rutas con nombre, que es lo que
|
|
7
|
-
* contrato del modelo necesita.
|
|
4
|
+
* El export por defecto es la tabla, igual que en la versión Vue. Se exponen
|
|
5
|
+
* además la lógica (por si alguien quiere pintarla de otra forma), las piezas
|
|
6
|
+
* con las que está hecha y el registro de rutas con nombre, que es lo que
|
|
7
|
+
* React Router no trae y el contrato del modelo necesita.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import DataTable from './src/DataTable.jsx'
|
|
@@ -15,3 +15,4 @@ export { default as useDataTable } from './src/useDataTable.js'
|
|
|
15
15
|
export { buildPath, hasRoute, registerRoutes, resetRoutes } from './src/routes.js'
|
|
16
16
|
export { default as DataTableComponent } from './src/components/DataTableComponent.jsx'
|
|
17
17
|
export { default as SelectPaginationComponent } from './src/components/SelectPaginationComponent.jsx'
|
|
18
|
+
export { DEFAULT_LABELS } from './src/table.js'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "innoboxrr-react-datatable",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Gemelo React de innoboxrr-vue-datatable: el mismo contrato de modelo, la misma tabla.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -37,11 +37,12 @@
|
|
|
37
37
|
"node": ">=20"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@
|
|
40
|
+
"@tanstack/react-table": "^9.2.4",
|
|
41
41
|
"axios": "^1.7.0"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"innoboxrr-form-core": "^2.
|
|
44
|
+
"innoboxrr-form-core": "^2.6.0",
|
|
45
|
+
"innoboxrr-react-form-elements": "^3.4.0",
|
|
45
46
|
"react": "^19.0.0",
|
|
46
47
|
"react-dom": "^19.0.0",
|
|
47
48
|
"react-router-dom": "^7.0.0"
|
|
@@ -51,6 +52,8 @@
|
|
|
51
52
|
"@testing-library/react": "^16.1.0",
|
|
52
53
|
"@testing-library/user-event": "^14.5.2",
|
|
53
54
|
"@vitejs/plugin-react": "^5.0.0",
|
|
55
|
+
"innoboxrr-form-core": "^2.6.0",
|
|
56
|
+
"innoboxrr-react-form-elements": "^3.4.0",
|
|
54
57
|
"jsdom": "^25.0.0",
|
|
55
58
|
"react": "^19.0.0",
|
|
56
59
|
"react-dom": "^19.0.0",
|
package/src/DataTable.jsx
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
2
|
-
import
|
|
1
|
+
import { useCallback, useId, useImperativeHandle, useMemo, useState } from 'react'
|
|
2
|
+
import { useNavigate } from 'react-router-dom'
|
|
3
|
+
import IconComponent from 'innoboxrr-react-form-elements/src/IconComponent.jsx'
|
|
4
|
+
import MenuComponent from 'innoboxrr-react-form-elements/src/MenuComponent.jsx'
|
|
5
|
+
|
|
3
6
|
import DataTableComponent from './components/DataTableComponent.jsx'
|
|
4
|
-
import NavDropdownComponent from './components/NavDropdownComponent.jsx'
|
|
5
7
|
import SelectPaginationComponent from './components/SelectPaginationComponent.jsx'
|
|
8
|
+
import { buildPath } from './routes.js'
|
|
9
|
+
import { DEFAULT_LABELS, menuItems } from './table.js'
|
|
6
10
|
import useDataTable from './useDataTable.js'
|
|
11
|
+
import useTheme, { joinClasses } from './useTheme.js'
|
|
7
12
|
|
|
8
13
|
/**
|
|
9
14
|
* Gemelo de DataTable.vue.
|
|
@@ -13,10 +18,11 @@ import useDataTable from './useDataTable.js'
|
|
|
13
18
|
* puras y llamadas HTTP, sin nada de un framework de UI. Por eso este
|
|
14
19
|
* componente recibe exactamente los mismos props que su gemelo.
|
|
15
20
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
21
|
+
* Las diferencias son de React: el slot `filterForm` es un prop, y lo que Vue
|
|
22
|
+
* expone con defineExpose —`refresh()`, `clearSelection()`— llega por `ref`.
|
|
18
23
|
*/
|
|
19
24
|
export default function DataTable({
|
|
25
|
+
ref = null,
|
|
20
26
|
dataUrl,
|
|
21
27
|
dataMethod = 'post',
|
|
22
28
|
model,
|
|
@@ -32,19 +38,46 @@ export default function DataTable({
|
|
|
32
38
|
hideColumns = [],
|
|
33
39
|
cardWrapper = true,
|
|
34
40
|
showTableHeader = true,
|
|
41
|
+
selectable = false,
|
|
42
|
+
labels = null,
|
|
35
43
|
filterForm = null,
|
|
36
44
|
}) {
|
|
45
|
+
const theme = useTheme()
|
|
46
|
+
const routerNavigate = useNavigate()
|
|
47
|
+
|
|
37
48
|
const [filtersOpen, setFiltersOpen] = useState(false)
|
|
49
|
+
const filtersId = useId()
|
|
50
|
+
|
|
51
|
+
const text = useMemo(() => ({ ...DEFAULT_LABELS, ...(labels ?? {}) }), [labels])
|
|
52
|
+
|
|
53
|
+
// React Router no tiene rutas con nombre: el contrato apunta a un nombre y
|
|
54
|
+
// `registerRoutes` dice a qué patrón corresponde.
|
|
55
|
+
const navigate = useCallback(
|
|
56
|
+
(target) => routerNavigate(buildPath(target.name, target.params, target.query)),
|
|
57
|
+
[routerNavigate]
|
|
58
|
+
)
|
|
38
59
|
|
|
39
60
|
const {
|
|
40
|
-
|
|
41
|
-
|
|
61
|
+
table,
|
|
62
|
+
visibleHead,
|
|
63
|
+
rows,
|
|
64
|
+
clones,
|
|
65
|
+
meta,
|
|
66
|
+
loading,
|
|
67
|
+
error,
|
|
68
|
+
sort,
|
|
69
|
+
orderBy,
|
|
42
70
|
crudActions,
|
|
43
|
-
|
|
71
|
+
bulkActions,
|
|
72
|
+
rowActions,
|
|
73
|
+
selectedIds,
|
|
74
|
+
refresh,
|
|
75
|
+
clearSelection,
|
|
44
76
|
sortColumn,
|
|
45
77
|
updatePage,
|
|
46
|
-
|
|
47
|
-
|
|
78
|
+
preparePolicies,
|
|
79
|
+
run,
|
|
80
|
+
runBulk,
|
|
48
81
|
} = useDataTable({
|
|
49
82
|
dataUrl,
|
|
50
83
|
dataMethod,
|
|
@@ -53,99 +86,135 @@ export default function DataTable({
|
|
|
53
86
|
policyMethod,
|
|
54
87
|
formFilters,
|
|
55
88
|
externalFilters,
|
|
89
|
+
extraParams,
|
|
90
|
+
extraQuery,
|
|
56
91
|
hideColumns,
|
|
92
|
+
selectable,
|
|
93
|
+
labels: text,
|
|
94
|
+
navigate,
|
|
57
95
|
})
|
|
58
96
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
97
|
+
useImperativeHandle(ref, () => ({ refresh, clearSelection, selectedIds, table }), [refresh, clearSelection, selectedIds, table])
|
|
98
|
+
|
|
99
|
+
const dataTableComponents = useMemo(
|
|
100
|
+
() => (typeof model.dataTableComponents === 'function' ? model.dataTableComponents() : {}),
|
|
101
|
+
[model]
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
const crudItems = useMemo(() => menuItems(crudActions, text, run), [crudActions, text, run])
|
|
105
|
+
const rowItems = useCallback((row) => menuItems(rowActions(row), text, run), [rowActions, text, run])
|
|
106
|
+
const prepareCrud = useCallback(() => preparePolicies(null), [preparePolicies])
|
|
107
|
+
|
|
108
|
+
const bulkVisible = selectable && selectedIds.length > 0
|
|
109
|
+
|
|
110
|
+
const renderBar = () => {
|
|
111
|
+
// Con filas seleccionadas, la barra dice cuántas y qué hacer con ellas,
|
|
112
|
+
// en el mismo sitio que la de siempre para que la tabla no salte.
|
|
113
|
+
if (bulkVisible) {
|
|
114
|
+
return (
|
|
115
|
+
<div className={theme.bulkBar} role="region" aria-label={text.selection}>
|
|
116
|
+
<span className={theme.bulkCount} aria-live="polite">{selectedIds.length} {text.selected}</span>
|
|
117
|
+
|
|
118
|
+
{bulkActions.map((action) => (
|
|
119
|
+
<button
|
|
120
|
+
key={action.id ?? action.name}
|
|
121
|
+
type="button"
|
|
122
|
+
className={joinClasses(action.danger ? theme.buttonDanger : theme.buttonSecondary, 'fe-button-sm')}
|
|
123
|
+
onClick={() => runBulk(action)}>
|
|
124
|
+
{action.icon ? <IconComponent name={action.icon} size={14} /> : null}
|
|
125
|
+
<span>{action.name}</span>
|
|
126
|
+
</button>
|
|
127
|
+
))}
|
|
128
|
+
|
|
129
|
+
<span className={theme.toolbarSpacer} />
|
|
130
|
+
|
|
131
|
+
<button type="button" className={theme.buttonLink} onClick={() => clearSelection()}>
|
|
132
|
+
{text.clearSelection}
|
|
133
|
+
</button>
|
|
134
|
+
</div>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (! showTopbar) {
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<div className={theme.toolbar}>
|
|
144
|
+
{hasActions ? (
|
|
145
|
+
<MenuComponent
|
|
146
|
+
items={crudItems}
|
|
147
|
+
label={text.actions}
|
|
148
|
+
placement="bottom-start"
|
|
149
|
+
beforeOpen={prepareCrud}
|
|
150
|
+
renderTrigger={({ toggle, loading: busy, triggerProps }) => (
|
|
151
|
+
<button
|
|
152
|
+
type="button"
|
|
153
|
+
className={theme.buttonSecondary}
|
|
154
|
+
{...triggerProps}
|
|
155
|
+
disabled={busy}
|
|
156
|
+
onClick={toggle}>
|
|
157
|
+
<IconComponent name="actions" size={14} />
|
|
158
|
+
<span>{text.actions}</span>
|
|
159
|
+
</button>
|
|
160
|
+
)} />
|
|
161
|
+
) : null}
|
|
162
|
+
|
|
163
|
+
<span className={theme.toolbarSpacer} />
|
|
164
|
+
|
|
165
|
+
<button type="button" className={theme.iconButton} aria-label={text.refresh} onClick={() => refresh()}>
|
|
166
|
+
<IconComponent name="refresh" size={16} />
|
|
167
|
+
</button>
|
|
168
|
+
|
|
169
|
+
{hasFilter ? (
|
|
170
|
+
<button
|
|
171
|
+
type="button"
|
|
172
|
+
className={theme.iconButton}
|
|
173
|
+
aria-label={text.filters}
|
|
174
|
+
aria-expanded={filtersOpen ? 'true' : 'false'}
|
|
175
|
+
aria-controls={filtersId}
|
|
176
|
+
onClick={() => setFiltersOpen((open) => ! open)}>
|
|
177
|
+
<IconComponent name="filter" size={16} />
|
|
178
|
+
</button>
|
|
179
|
+
) : null}
|
|
180
|
+
</div>
|
|
181
|
+
)
|
|
182
|
+
}
|
|
62
183
|
|
|
63
184
|
return (
|
|
64
|
-
<
|
|
65
|
-
{
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
<div className="fe-w-expand">
|
|
71
|
-
<button
|
|
72
|
-
type="button"
|
|
73
|
-
className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 mr-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
|
74
|
-
onClick={() => actionButtonClicked(crudActions)}>
|
|
75
|
-
Acciones
|
|
76
|
-
</button>
|
|
77
|
-
|
|
78
|
-
<NavDropdownComponent id="actionCrudDropdown" pos="right">
|
|
79
|
-
<ActionListComponent
|
|
80
|
-
actions={crudActions}
|
|
81
|
-
extraParams={extraParams}
|
|
82
|
-
extraQuery={extraQuery}
|
|
83
|
-
onActionClicked={actionClicked} />
|
|
84
|
-
</NavDropdownComponent>
|
|
85
|
-
</div>
|
|
86
|
-
) : (
|
|
87
|
-
<div><div className="fe-w-expand"></div></div>
|
|
88
|
-
)}
|
|
89
|
-
|
|
90
|
-
{hasFilter ? (
|
|
91
|
-
<div className="fe-w-auto">
|
|
92
|
-
<div className="fe-grid-divider fe-children-expand fe-text-center" fe-grid="">
|
|
93
|
-
<div>
|
|
94
|
-
<button
|
|
95
|
-
type="button"
|
|
96
|
-
aria-label="Update results"
|
|
97
|
-
className="fe-text-right pointer"
|
|
98
|
-
onClick={updateFilters}>
|
|
99
|
-
<svg className="w-6 h-6 text-slate-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 20">
|
|
100
|
-
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 1v5h-5M2 19v-5h5m10-4a8 8 0 0 1-14.947 3.97M1 10a8 8 0 0 1 14.947-3.97" />
|
|
101
|
-
</svg>
|
|
102
|
-
</button>
|
|
103
|
-
</div>
|
|
104
|
-
|
|
105
|
-
<div>
|
|
106
|
-
<button
|
|
107
|
-
type="button"
|
|
108
|
-
aria-label="Buscar"
|
|
109
|
-
aria-expanded={filtersOpen}
|
|
110
|
-
className="fe-text-right pointer"
|
|
111
|
-
onClick={() => setFiltersOpen((open) => ! open)}>
|
|
112
|
-
<svg className="w-6 h-6 text-slate-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 18">
|
|
113
|
-
<path d="M18.85 1.1A1.99 1.99 0 0 0 17.063 0H2.937a2 2 0 0 0-1.566 3.242L6.99 9.868 7 14a1 1 0 0 0 .4.8l4 3A1 1 0 0 0 13 17l.01-7.134 5.66-6.676a1.99 1.99 0 0 0 .18-2.09Z" />
|
|
114
|
-
</svg>
|
|
115
|
-
</button>
|
|
116
|
-
</div>
|
|
117
|
-
</div>
|
|
118
|
-
</div>
|
|
119
|
-
) : null}
|
|
120
|
-
</div>
|
|
121
|
-
</div>
|
|
122
|
-
|
|
123
|
-
{hasFilter ? (
|
|
124
|
-
// El uk-toggle de la version Vue trabaja sobre el DOM
|
|
125
|
-
// por su cuenta; en React el estado gobierna la vista.
|
|
126
|
-
<div className="filter-form fe-card fe-card-body fe-pt-0" hidden={! filtersOpen}>
|
|
127
|
-
{filterForm}
|
|
128
|
-
</div>
|
|
129
|
-
) : null}
|
|
185
|
+
<section className={joinClasses(theme.datatable, cardWrapper && theme.surface)}>
|
|
186
|
+
{renderBar()}
|
|
187
|
+
|
|
188
|
+
{showTopbar && hasFilter ? (
|
|
189
|
+
<div id={filtersId} className={joinClasses('filter-form', theme.datatableFilters)} hidden={! filtersOpen}>
|
|
190
|
+
{filterForm}
|
|
130
191
|
</div>
|
|
131
192
|
) : null}
|
|
132
193
|
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
194
|
+
<DataTableComponent
|
|
195
|
+
table={table}
|
|
196
|
+
head={visibleHead}
|
|
197
|
+
rows={rows}
|
|
198
|
+
clones={clones}
|
|
199
|
+
loading={loading}
|
|
200
|
+
error={error}
|
|
201
|
+
actions={hasActions}
|
|
202
|
+
selectable={selectable}
|
|
203
|
+
showTableHeader={showTableHeader}
|
|
204
|
+
dataTableComponents={dataTableComponents}
|
|
205
|
+
labels={text}
|
|
206
|
+
orderBy={orderBy}
|
|
207
|
+
sort={sort}
|
|
208
|
+
itemsFor={rowItems}
|
|
209
|
+
prepareRow={preparePolicies}
|
|
210
|
+
onSortColumn={sortColumn}
|
|
211
|
+
onRetry={refresh} />
|
|
212
|
+
|
|
213
|
+
{/* Con un error y sin filas, un «No hay resultados» en el pie
|
|
214
|
+
contradiría el motivo que ya da la tabla. */}
|
|
215
|
+
{error && rows.length === 0 ? null : (
|
|
216
|
+
<SelectPaginationComponent meta={meta} labels={text} onPageChange={updatePage} />
|
|
217
|
+
)}
|
|
218
|
+
</section>
|
|
150
219
|
)
|
|
151
220
|
}
|