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/src/useDataTable.js
CHANGED
|
@@ -1,45 +1,35 @@
|
|
|
1
1
|
import axios from 'axios'
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
3
|
+
import { flushSync } from 'react-dom'
|
|
4
|
+
import {
|
|
5
|
+
columnVisibilityFeature,
|
|
6
|
+
rowSelectionFeature,
|
|
7
|
+
rowSortingFeature,
|
|
8
|
+
tableFeatures,
|
|
9
|
+
useTable,
|
|
10
|
+
} from '@tanstack/react-table'
|
|
11
|
+
import { notifyError, notifySuccess } from 'innoboxrr-form-core'
|
|
12
|
+
|
|
13
|
+
import * as core from './table.js'
|
|
3
14
|
|
|
4
15
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
16
|
+
* Solo lo que la tabla usa. El orden y la paginación los hace el servidor, así
|
|
17
|
+
* que no hay modelos de filas ordenadas ni paginadas: TanStack Table lleva el
|
|
18
|
+
* estado —qué columnas se ven, qué filas están seleccionadas, por qué columna
|
|
19
|
+
* se ordena— y la tabla pinta.
|
|
7
20
|
*/
|
|
8
|
-
|
|
9
|
-
?? document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
|
|
10
|
-
?? ''
|
|
21
|
+
const features = tableFeatures({ rowSortingFeature, columnVisibilityFeature, rowSelectionFeature })
|
|
11
22
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
*/
|
|
16
|
-
export const isEqual = (a, b) => {
|
|
17
|
-
if (a === b) {
|
|
18
|
-
return true
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) {
|
|
22
|
-
return false
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const keysA = Object.keys(a)
|
|
26
|
-
const keysB = Object.keys(b)
|
|
27
|
-
|
|
28
|
-
return keysA.length === keysB.length && keysA.every((key) => isEqual(a[key], b[key]))
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const hiddenColumnIds = (hideColumns) => hideColumns.map(
|
|
32
|
-
// Se admite tanto ['name'] como [{ id: 'name' }]: el contrato nunca estuvo
|
|
33
|
-
// documentado y por ahí circulan las dos formas.
|
|
34
|
-
(column) => (typeof column === 'string' ? column : column?.id)
|
|
35
|
-
)
|
|
23
|
+
// Un array nuevo en cada render invalidaría los modelos de TanStack Table.
|
|
24
|
+
const NO_ROWS = []
|
|
25
|
+
const NO_SELECTION = {}
|
|
36
26
|
|
|
37
27
|
/**
|
|
38
|
-
* Toda la lógica de la tabla: cargar, ordenar, paginar y resolver
|
|
28
|
+
* Toda la lógica de la tabla: cargar, ordenar, paginar, seleccionar y resolver
|
|
29
|
+
* permisos. Es la misma que `useDataTable` de innoboxrr-vue-datatable.
|
|
39
30
|
*
|
|
40
|
-
* Está fuera del componente
|
|
41
|
-
*
|
|
42
|
-
* quiere pintar la tabla de otra forma.
|
|
31
|
+
* Está fuera del componente para poder probarla y para pintar la misma tabla
|
|
32
|
+
* de otra forma.
|
|
43
33
|
*/
|
|
44
34
|
export default function useDataTable({
|
|
45
35
|
dataUrl,
|
|
@@ -49,208 +39,360 @@ export default function useDataTable({
|
|
|
49
39
|
policyMethod = 'post',
|
|
50
40
|
formFilters = {},
|
|
51
41
|
externalFilters = {},
|
|
42
|
+
extraParams = {},
|
|
43
|
+
extraQuery = {},
|
|
52
44
|
hideColumns = [],
|
|
45
|
+
selectable = false,
|
|
46
|
+
labels = core.DEFAULT_LABELS,
|
|
47
|
+
navigate = null,
|
|
53
48
|
}) {
|
|
54
|
-
const head = useMemo(() =>
|
|
55
|
-
|
|
49
|
+
const head = useMemo(() => model.dataTableHead(), [model])
|
|
50
|
+
const columns = useMemo(() => core.columnsFrom(head), [head])
|
|
51
|
+
|
|
52
|
+
// Los props llegan como objetos nuevos en cada render del padre: se
|
|
53
|
+
// compara su contenido, no su identidad.
|
|
54
|
+
const hiddenKey = core.snapshot(core.hiddenColumnIds(hideColumns))
|
|
55
|
+
const formKey = core.snapshot(formFilters)
|
|
56
|
+
const externalKey = core.snapshot(externalFilters)
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
const visibleHead = useMemo(() => {
|
|
59
|
+
const hidden = JSON.parse(hiddenKey)
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
61
|
+
return head.filter((column) => ! hidden.includes(column.id))
|
|
62
|
+
}, [head, hiddenKey])
|
|
63
|
+
|
|
64
|
+
const columnVisibility = useMemo(() => core.visibilityFrom(JSON.parse(hiddenKey)), [hiddenKey])
|
|
65
|
+
|
|
66
|
+
const [rows, setRows] = useState(NO_ROWS)
|
|
67
|
+
const [meta, setMeta] = useState({})
|
|
68
|
+
const [links, setLinks] = useState(NO_ROWS)
|
|
69
|
+
const [loading, setLoading] = useState(false)
|
|
70
|
+
const [error, setError] = useState(null)
|
|
71
|
+
const [sort, setSort] = useState(() => ({ ...model.dataTableSort() }))
|
|
64
72
|
const [orderBy, setOrderBy] = useState('id')
|
|
65
73
|
const [page, setPage] = useState(1)
|
|
66
74
|
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
const policyAttempts = useRef(0)
|
|
70
|
-
const timers = useRef([])
|
|
75
|
+
/** Lo que respondió el backend de permisos, por fila y para la barra. */
|
|
76
|
+
const [allowed, setAllowed] = useState({})
|
|
71
77
|
|
|
72
|
-
|
|
73
|
-
|
|
78
|
+
const sorting = useMemo(() => core.sortingFrom(orderBy, sort), [orderBy, sort])
|
|
79
|
+
|
|
80
|
+
const table = useTable({
|
|
81
|
+
features,
|
|
82
|
+
columns,
|
|
83
|
+
data: rows,
|
|
84
|
+
getRowId: (row, index) => String(row?.id ?? index),
|
|
85
|
+
manualSorting: true,
|
|
86
|
+
enableRowSelection: selectable === true,
|
|
87
|
+
isRowRangeSelectionEvent: core.isRangeEvent,
|
|
88
|
+
state: { columnVisibility, sorting },
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const rowSelection = table.state.rowSelection ?? NO_SELECTION
|
|
92
|
+
const selectedIds = useMemo(() => Object.keys(rowSelection).filter((id) => rowSelection[id]), [rowSelection])
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Una copia por fila y carga, para que un parser del modelo no pueda mutar
|
|
96
|
+
* los datos de la tabla.
|
|
97
|
+
*/
|
|
98
|
+
const clones = useMemo(() => rows.map(core.cloneRow), [rows])
|
|
99
|
+
|
|
100
|
+
const crudActions = useMemo(
|
|
101
|
+
() => core.withPolicies(model.crudActions(), allowed[core.CRUD_POLICIES]),
|
|
102
|
+
[model, allowed]
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
const bulkActions = useMemo(
|
|
106
|
+
() => (typeof model.bulkActions === 'function' ? model.bulkActions() : NO_ROWS),
|
|
107
|
+
[model]
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
const rowActions = useCallback(
|
|
111
|
+
(row) => core.withPolicies(row?.actions ?? NO_ROWS, allowed[String(row?.id)]),
|
|
112
|
+
[allowed]
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
// Lo que las funciones asíncronas leen cuando terminan: el valor de ese
|
|
116
|
+
// momento, no el del render en que empezaron.
|
|
74
117
|
const latest = useRef({})
|
|
75
|
-
latest.current = { dataUrl, dataMethod, model, formFilters, externalFilters, orderBy, sort, page }
|
|
76
118
|
|
|
77
|
-
|
|
78
|
-
|
|
119
|
+
latest.current = {
|
|
120
|
+
dataUrl, dataMethod, model, policyUrl, policyMethod, formFilters, externalFilters,
|
|
121
|
+
extraParams, extraQuery, labels, navigate, orderBy, sort, page, rows, allowed, table,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const internalSort = useRef(false)
|
|
125
|
+
const requestId = useRef(0)
|
|
126
|
+
const mounted = useRef(false)
|
|
79
127
|
|
|
80
|
-
|
|
81
|
-
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
mounted.current = true
|
|
82
130
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
? { ...params, ...form, ...external, ...ordering, page: current }
|
|
87
|
-
: { ...params, ...form, ...ordering, ...external, page: current }
|
|
131
|
+
return () => {
|
|
132
|
+
mounted.current = false
|
|
133
|
+
}
|
|
88
134
|
}, [])
|
|
89
135
|
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
const
|
|
136
|
+
const load = useCallback(async () => {
|
|
137
|
+
const id = ++requestId.current
|
|
138
|
+
const current = latest.current
|
|
139
|
+
|
|
140
|
+
const filters = core.requestFilters({
|
|
141
|
+
formFilters: current.formFilters,
|
|
142
|
+
externalFilters: current.externalFilters,
|
|
143
|
+
orderBy: current.orderBy,
|
|
144
|
+
sort: current.sort,
|
|
145
|
+
page: current.page,
|
|
146
|
+
internalSort: internalSort.current,
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
current.model.setFilters?.(filters)
|
|
150
|
+
setLoading(true)
|
|
151
|
+
|
|
152
|
+
// Una respuesta que llega tarde no pisa a la de una petición posterior.
|
|
153
|
+
const stale = () => id !== requestId.current || ! mounted.current
|
|
93
154
|
|
|
94
155
|
try {
|
|
95
|
-
const response = await axios(
|
|
96
|
-
method,
|
|
97
|
-
url,
|
|
98
|
-
data: method === 'post' ? filters : null,
|
|
99
|
-
params: method === 'get' ? filters : null,
|
|
100
|
-
})
|
|
156
|
+
const response = await axios(core.requestConfig(current.dataMethod, current.dataUrl, filters))
|
|
101
157
|
|
|
102
|
-
|
|
103
|
-
setBody(response.data.data)
|
|
104
|
-
setPagination({ meta: response.data.meta, links: response.data.links })
|
|
105
|
-
} catch (error) {
|
|
106
|
-
// Un fallo de red no trae respuesta: leer error.response.status sin
|
|
107
|
-
// comprobarlo lanzaba un TypeError dentro del propio manejador.
|
|
108
|
-
if (error.response?.status === 403) {
|
|
158
|
+
if (stale()) {
|
|
109
159
|
return
|
|
110
160
|
}
|
|
111
161
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
162
|
+
setRows(response.data?.data ?? NO_ROWS)
|
|
163
|
+
setMeta(response.data?.meta ?? {})
|
|
164
|
+
setLinks(response.data?.links ?? NO_ROWS)
|
|
165
|
+
setError(null)
|
|
166
|
+
|
|
167
|
+
// Con datos nuevos los permisos se vuelven a preguntar.
|
|
168
|
+
setAllowed({})
|
|
169
|
+
} catch (failure) {
|
|
170
|
+
if (stale()) {
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const described = core.describeError(failure, latest.current.labels)
|
|
175
|
+
|
|
176
|
+
if (described.status === 403) {
|
|
177
|
+
setRows(NO_ROWS)
|
|
178
|
+
setMeta({})
|
|
179
|
+
} else if (latest.current.rows.length > 0) {
|
|
180
|
+
// Con filas en pantalla el error no tiene sitio en la tabla:
|
|
181
|
+
// se avisa y se deja lo que había.
|
|
182
|
+
notifyError(described.message)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
setError(described)
|
|
186
|
+
} finally {
|
|
187
|
+
if (! stale()) {
|
|
188
|
+
setLoading(false)
|
|
117
189
|
}
|
|
118
190
|
}
|
|
119
|
-
}, [
|
|
191
|
+
}, [])
|
|
192
|
+
|
|
193
|
+
const clearSelection = useCallback(() => {
|
|
194
|
+
latest.current.table.resetRowSelection(true)
|
|
195
|
+
}, [])
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Una sola petición por cambio. Un filtro nuevo en otra página primero
|
|
199
|
+
* vuelve a la 1 y carga en la pasada siguiente; antes eran dos peticiones
|
|
200
|
+
* iguales, porque la página y los filtros se vigilaban por separado.
|
|
201
|
+
*/
|
|
202
|
+
const seen = useRef({ formKey, externalKey })
|
|
203
|
+
const sortKey = core.snapshot(sort)
|
|
204
|
+
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
const previous = seen.current
|
|
207
|
+
|
|
208
|
+
seen.current = { formKey, externalKey }
|
|
209
|
+
|
|
210
|
+
// Un filtro nuevo o externo descarta la selección, que era de otro
|
|
211
|
+
// listado; solo el del formulario vuelve a la primera página.
|
|
212
|
+
if (previous.formKey !== formKey || previous.externalKey !== externalKey) {
|
|
213
|
+
clearSelection()
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (previous.formKey !== formKey && page !== 1) {
|
|
217
|
+
setPage(1)
|
|
218
|
+
|
|
219
|
+
return
|
|
220
|
+
}
|
|
120
221
|
|
|
121
|
-
|
|
122
|
-
|
|
222
|
+
load()
|
|
223
|
+
}, [formKey, externalKey, orderBy, sortKey, page, load, clearSelection])
|
|
123
224
|
|
|
124
|
-
|
|
125
|
-
}, [fetchData, getFilters])
|
|
225
|
+
const refresh = useCallback(() => load(), [load])
|
|
126
226
|
|
|
127
227
|
const sortColumn = useCallback((column) => {
|
|
128
|
-
if (column
|
|
228
|
+
if (column?.sortable !== true) {
|
|
129
229
|
return
|
|
130
230
|
}
|
|
131
231
|
|
|
132
|
-
// A partir de aquí el orden elegido por el usuario manda sobre el que
|
|
133
|
-
// puedan traer los filtros externos.
|
|
134
232
|
internalSort.current = true
|
|
135
233
|
|
|
234
|
+
setSort((current) => core.toggledSort(current, column.id))
|
|
136
235
|
setOrderBy(column.id)
|
|
137
|
-
setSort((current) => {
|
|
138
|
-
const next = { ...current, [column.id]: current[column.id] === 'asc' ? 'desc' : 'asc' }
|
|
139
|
-
|
|
140
|
-
latest.current.sort = next
|
|
141
|
-
latest.current.orderBy = column.id
|
|
142
|
-
|
|
143
|
-
return next
|
|
144
|
-
})
|
|
145
236
|
}, [])
|
|
146
237
|
|
|
147
238
|
const updatePage = useCallback((next) => {
|
|
148
|
-
|
|
149
|
-
setPage(next)
|
|
150
|
-
}, [])
|
|
239
|
+
const value = Number(next)
|
|
151
240
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
await latest.current.model[action.callback](action.params)
|
|
155
|
-
|
|
156
|
-
return updateFilters()
|
|
157
|
-
} catch (error) {
|
|
158
|
-
console.error(error)
|
|
241
|
+
if (Number.isInteger(value) && value >= 1) {
|
|
242
|
+
setPage(value)
|
|
159
243
|
}
|
|
160
|
-
}, [
|
|
244
|
+
}, [])
|
|
161
245
|
|
|
162
246
|
/**
|
|
163
|
-
* Pregunta al backend qué
|
|
164
|
-
*
|
|
247
|
+
* Pregunta al backend qué se puede hacer con una fila —o con la barra, sin
|
|
248
|
+
* id— antes de abrir su menú. Lo que responde se guarda hasta la próxima
|
|
249
|
+
* carga.
|
|
165
250
|
*/
|
|
166
|
-
const
|
|
167
|
-
const
|
|
251
|
+
const preparePolicies = useCallback(async (id = null) => {
|
|
252
|
+
const key = id == null ? core.CRUD_POLICIES : String(id)
|
|
253
|
+
const current = latest.current
|
|
254
|
+
|
|
255
|
+
if (current.allowed[key]) {
|
|
256
|
+
return
|
|
257
|
+
}
|
|
168
258
|
|
|
169
259
|
try {
|
|
170
|
-
const response = await axios({
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
260
|
+
const response = await axios(core.requestConfig(current.policyMethod, current.policyUrl, {
|
|
261
|
+
_token: core.csrfToken(),
|
|
262
|
+
id,
|
|
263
|
+
}))
|
|
264
|
+
|
|
265
|
+
if (! mounted.current) {
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Se pinta ya: el menú se abre justo después, y tiene que abrir con
|
|
270
|
+
// los permisos puestos y no enseñarlos cambiando.
|
|
271
|
+
flushSync(() => {
|
|
272
|
+
setAllowed((previous) => ({ ...previous, [key]: response.data ?? {} }))
|
|
175
273
|
})
|
|
274
|
+
} catch {
|
|
275
|
+
// El menú se abre igual, con todo deshabilitado.
|
|
276
|
+
notifyError(latest.current.labels.policiesFailed)
|
|
277
|
+
}
|
|
278
|
+
}, [])
|
|
176
279
|
|
|
177
|
-
|
|
280
|
+
const run = useCallback(async (action) => {
|
|
281
|
+
const current = latest.current
|
|
282
|
+
const text = current.labels
|
|
283
|
+
const kind = core.actionKind(action)
|
|
178
284
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
285
|
+
if (kind === 'route') {
|
|
286
|
+
if (! current.navigate) {
|
|
287
|
+
notifyError(text.actionFailed)
|
|
182
288
|
|
|
183
|
-
|
|
184
|
-
// repinta nada, así que se sustituye la lista.
|
|
185
|
-
if (actions === crudActions) {
|
|
186
|
-
setCrudActions(allowed)
|
|
187
|
-
} else {
|
|
188
|
-
setBody((rows) => rows.map((row) => (
|
|
189
|
-
row.actions === actions ? { ...row, actions: allowed } : row
|
|
190
|
-
)))
|
|
289
|
+
return undefined
|
|
191
290
|
}
|
|
192
291
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
292
|
+
try {
|
|
293
|
+
await current.navigate(core.routeTarget(action, current.extraParams, current.extraQuery))
|
|
294
|
+
} catch (failure) {
|
|
295
|
+
// Una ruta que no existe es un error de quien declaró la
|
|
296
|
+
// acción: el usuario recibe un aviso y la consola, el detalle.
|
|
297
|
+
notifyError(text.actionFailed)
|
|
298
|
+
console.error(failure)
|
|
299
|
+
}
|
|
200
300
|
|
|
201
|
-
|
|
301
|
+
return undefined
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (kind === 'link') {
|
|
305
|
+
globalThis.window?.open(action.params?.link, action.params?.target ?? '_self')
|
|
306
|
+
|
|
307
|
+
return undefined
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (typeof current.model[action.callback] !== 'function') {
|
|
311
|
+
notifyError(text.actionFailed)
|
|
312
|
+
|
|
313
|
+
return undefined
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
await current.model[action.callback](action.params)
|
|
318
|
+
} catch (failure) {
|
|
319
|
+
if (! core.isCancelled(failure)) {
|
|
320
|
+
notifyError(core.describeError(failure, text, text.actionFailed).message)
|
|
202
321
|
}
|
|
203
322
|
|
|
204
|
-
|
|
205
|
-
policyAttempts.current = 0
|
|
206
|
-
}, 3000))
|
|
323
|
+
return undefined
|
|
207
324
|
}
|
|
208
|
-
}, [policyMethod, policyUrl, crudActions])
|
|
209
325
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
214
|
-
}, [orderBy, sort, page])
|
|
326
|
+
if (action.success) {
|
|
327
|
+
notifySuccess(action.success)
|
|
328
|
+
}
|
|
215
329
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const previousExternal = useRef(externalFilters)
|
|
330
|
+
return load()
|
|
331
|
+
}, [load])
|
|
219
332
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
333
|
+
/**
|
|
334
|
+
* Una acción masiva recibe los ids seleccionados —también los de otras
|
|
335
|
+
* páginas— y las filas cargadas que están entre ellos.
|
|
336
|
+
*/
|
|
337
|
+
const runBulk = useCallback(async (action) => {
|
|
338
|
+
const current = latest.current
|
|
339
|
+
const text = current.labels
|
|
340
|
+
|
|
341
|
+
if (typeof current.model[action.callback] !== 'function') {
|
|
342
|
+
notifyError(text.actionFailed)
|
|
343
|
+
|
|
344
|
+
return undefined
|
|
223
345
|
}
|
|
224
346
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}, [formFilters, updateFilters, updatePage])
|
|
347
|
+
const selected = current.table.state.rowSelection ?? NO_SELECTION
|
|
348
|
+
const ids = Object.keys(selected).filter((id) => selected[id])
|
|
349
|
+
const loaded = current.table.getSelectedRowModel().rows.map((row) => row.original)
|
|
229
350
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
351
|
+
try {
|
|
352
|
+
await current.model[action.callback](ids, loaded)
|
|
353
|
+
} catch (failure) {
|
|
354
|
+
if (! core.isCancelled(failure)) {
|
|
355
|
+
notifyError(core.describeError(failure, text, text.actionFailed).message)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return undefined
|
|
233
359
|
}
|
|
234
360
|
|
|
235
|
-
|
|
236
|
-
updateFilters()
|
|
237
|
-
}, [externalFilters, updateFilters])
|
|
361
|
+
clearSelection()
|
|
238
362
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
363
|
+
if (action.success) {
|
|
364
|
+
notifySuccess(action.success)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return load()
|
|
368
|
+
}, [clearSelection, load])
|
|
242
369
|
|
|
243
370
|
return {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
371
|
+
table,
|
|
372
|
+
head,
|
|
373
|
+
visibleHead,
|
|
374
|
+
rows,
|
|
375
|
+
clones,
|
|
376
|
+
meta,
|
|
377
|
+
links,
|
|
378
|
+
loading,
|
|
379
|
+
error,
|
|
247
380
|
sort,
|
|
248
381
|
orderBy,
|
|
249
382
|
page,
|
|
250
|
-
|
|
383
|
+
crudActions,
|
|
384
|
+
bulkActions,
|
|
385
|
+
rowActions,
|
|
386
|
+
selectedIds,
|
|
387
|
+
// Lo que devolvía la versión anterior, para quien lo lea desde fuera.
|
|
388
|
+
dataTable: { head: visibleHead, body: rows },
|
|
389
|
+
pagination: { meta, links },
|
|
390
|
+
refresh,
|
|
391
|
+
clearSelection,
|
|
251
392
|
sortColumn,
|
|
252
393
|
updatePage,
|
|
253
|
-
|
|
254
|
-
|
|
394
|
+
preparePolicies,
|
|
395
|
+
run,
|
|
396
|
+
runBulk,
|
|
255
397
|
}
|
|
256
398
|
}
|
package/src/useTheme.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useSyncExternalStore } from 'react'
|
|
2
|
+
import { getTheme, onThemeChange } from 'innoboxrr-form-core'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* El tema de innoboxrr-form-core.
|
|
6
|
+
*
|
|
7
|
+
* Va por `useSyncExternalStore` porque el tema es estado de módulo: un
|
|
8
|
+
* `setTheme` con la tabla ya montada tiene que repintarla.
|
|
9
|
+
*
|
|
10
|
+
* @returns {Record<string, string>}
|
|
11
|
+
*/
|
|
12
|
+
export default function useTheme() {
|
|
13
|
+
return useSyncExternalStore(onThemeChange, () => getTheme(), () => getTheme())
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Une clases descartando las vacías. */
|
|
17
|
+
export const joinClasses = (...classes) => classes.filter(Boolean).join(' ')
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import DisabledLinkComponent from './DisabledLinkComponent.jsx'
|
|
2
|
-
import IconLinkComponent from './IconLinkComponent.jsx'
|
|
3
|
-
import IconRouteComponent from './IconRouteComponent.jsx'
|
|
4
|
-
|
|
5
|
-
const closeDropdown = (event) => {
|
|
6
|
-
const dropdown = event.target.closest('.uk-dropdown')
|
|
7
|
-
|
|
8
|
-
// UIkit lo aporta la aplicación anfitriona.
|
|
9
|
-
if (dropdown) {
|
|
10
|
-
globalThis.UIkit?.dropdown(dropdown)?.hide(false)
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Las tres formas que puede tomar una acción del contrato del modelo, en un
|
|
16
|
-
* solo sitio. En la versión Vue este bloque está copiado en `DataTable.vue` y
|
|
17
|
-
* en `DataTableComponent.vue`, con una diferencia entre ambas copias — la de
|
|
18
|
-
* dentro soporta `action.link` y la de fuera no.
|
|
19
|
-
*/
|
|
20
|
-
export default function ActionListComponent({ actions = [], extraParams = {}, extraQuery = {}, onActionClicked }) {
|
|
21
|
-
return actions.map((action) => (
|
|
22
|
-
<li key={action.id ?? action.name} className="hover:bg-slate-100 dark:hover:bg-slate-600 px-2 py-1">
|
|
23
|
-
{(() => {
|
|
24
|
-
if (! action.policy) {
|
|
25
|
-
return <DisabledLinkComponent icon={action.icon} text={action.name} />
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (action.route && ! action.link) {
|
|
29
|
-
return (
|
|
30
|
-
<IconRouteComponent
|
|
31
|
-
name={action.params.to.name}
|
|
32
|
-
params={{ ...action.params.to.params, ...extraParams }}
|
|
33
|
-
query={action.params.to.query ? { ...action.params.to.query, ...extraQuery } : { ...extraQuery }}
|
|
34
|
-
icon={action.icon}
|
|
35
|
-
text={action.name} />
|
|
36
|
-
)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (action.route && action.link) {
|
|
40
|
-
return (
|
|
41
|
-
<IconLinkComponent
|
|
42
|
-
link={action.params.link}
|
|
43
|
-
target={action.params.target}
|
|
44
|
-
icon={action.icon}
|
|
45
|
-
text={action.name} />
|
|
46
|
-
)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return (
|
|
50
|
-
<IconLinkComponent
|
|
51
|
-
icon={action.icon}
|
|
52
|
-
text={action.name}
|
|
53
|
-
onClick={(event) => {
|
|
54
|
-
event.preventDefault()
|
|
55
|
-
onActionClicked?.(action)
|
|
56
|
-
closeDropdown(event)
|
|
57
|
-
}} />
|
|
58
|
-
)
|
|
59
|
-
})()}
|
|
60
|
-
</li>
|
|
61
|
-
))
|
|
62
|
-
}
|