innoboxrr-vue-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 +130 -168
- package/index.js +13 -2
- package/package.json +6 -3
- package/src/DataTable.vue +249 -423
- package/src/components/DataTableComponent.vue +218 -176
- package/src/components/SelectPaginationComponent.vue +78 -93
- package/src/table.js +251 -0
- package/src/useDataTable.js +370 -0
- package/src/useTheme.js +20 -0
- package/src/components/DisabledLinkComponent.vue +0 -45
- package/src/components/IconLinkComponent.vue +0 -81
- package/src/components/IconRouteComponent.vue +0 -84
- package/src/components/NavDropdownComponent.vue +0 -55
- package/src/components/PaginationComponent.vue +0 -79
package/src/DataTable.vue
CHANGED
|
@@ -1,430 +1,256 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div>
|
|
3
|
-
<div v-if="showTopbar">
|
|
4
|
-
<div class="fe-container fe-container-wide pt-4">
|
|
5
|
-
<div fe-grid>
|
|
6
|
-
<div class="fe-w-expand" v-if="hasActions">
|
|
7
|
-
<button
|
|
8
|
-
class="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"
|
|
9
|
-
@click="actionButtonClicked(crudActions)">
|
|
10
|
-
Acciones
|
|
11
|
-
</button>
|
|
12
|
-
<NavDropdownComponent id="actionCrudDropdown" pos="right">
|
|
13
|
-
<li v-for="action in crudActions" :key="action.id">
|
|
14
|
-
<template v-if="action.route">
|
|
15
|
-
<IconRouteComponent
|
|
16
|
-
v-if="action.policy"
|
|
17
|
-
:name="action.params.to.name"
|
|
18
|
-
:params="{...action.params.to.params, ...extraParams}"
|
|
19
|
-
:query="action.params.to.query ? {...action.params.to.query, ...extraQuery} : {...extraQuery}"
|
|
20
|
-
:icon="action.icon"
|
|
21
|
-
:text="action.name" />
|
|
22
|
-
<DisabledLinkComponent
|
|
23
|
-
v-else
|
|
24
|
-
:icon="action.icon"
|
|
25
|
-
:text="action.name"/>
|
|
26
|
-
</template>
|
|
27
|
-
<template v-else>
|
|
28
|
-
<IconLinkComponent
|
|
29
|
-
v-if="action.policy"
|
|
30
|
-
:icon="action.icon"
|
|
31
|
-
:text="action.name"
|
|
32
|
-
@click="actionClicked(action) , closeDropdown($event)" />
|
|
33
|
-
<DisabledLinkComponent
|
|
34
|
-
v-else
|
|
35
|
-
:icon="action.icon"
|
|
36
|
-
:text="action.name"/>
|
|
37
|
-
</template>
|
|
38
|
-
</li>
|
|
39
|
-
</NavDropdownComponent>
|
|
40
|
-
</div>
|
|
41
|
-
<div v-else>
|
|
42
|
-
<div class="fe-w-expand"></div>
|
|
43
|
-
</div>
|
|
44
|
-
<div v-if="hasFilter" class="fe-w-auto">
|
|
45
|
-
<div class="fe-grid-divider fe-children-expand fe-text-center" fe-grid>
|
|
46
|
-
<div>
|
|
47
|
-
<button
|
|
48
|
-
type="button"
|
|
49
|
-
class="fe-text-right pointer"
|
|
50
|
-
data-tooltip="Update results"
|
|
51
|
-
aria-label="Update results"
|
|
52
|
-
@click="updateFilters">
|
|
53
|
-
<svg class="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">
|
|
54
|
-
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="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"/>
|
|
55
|
-
</svg>
|
|
56
|
-
</button>
|
|
57
|
-
</div>
|
|
58
|
-
<div>
|
|
59
|
-
<button
|
|
60
|
-
type="button"
|
|
61
|
-
class="fe-text-right pointer"
|
|
62
|
-
uk-toggle="target: .filter-form; animation: uk-animation-scale-up;"
|
|
63
|
-
data-tooltip="Buscar"
|
|
64
|
-
aria-label="Buscar">
|
|
65
|
-
<svg class="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">
|
|
66
|
-
<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"/>
|
|
67
|
-
</svg>
|
|
68
|
-
</button>
|
|
69
|
-
</div>
|
|
70
|
-
</div>
|
|
71
|
-
</div>
|
|
72
|
-
</div>
|
|
73
|
-
</div>
|
|
74
|
-
<div
|
|
75
|
-
v-if="hasFilter"
|
|
76
|
-
class="filter-form fe-card fe-card-body fe-pt-0"
|
|
77
|
-
hidden>
|
|
78
|
-
<slot name="filterForm"></slot>
|
|
79
|
-
</div>
|
|
80
|
-
</div>
|
|
81
|
-
<div
|
|
82
|
-
class="fe-container fe-container-wide"
|
|
83
|
-
:class="{ 'ptb-20': showTopbar }">
|
|
84
|
-
<div
|
|
85
|
-
class="fe-p-sm"
|
|
86
|
-
:class="{ 'bg-white p-6 rounded-lg shadow dark:border-slate-700 dark:bg-slate-800': cardWrapper }">
|
|
87
|
-
<DataTableComponent
|
|
88
|
-
:actions="hasActions"
|
|
89
|
-
:data-table="dataTable"
|
|
90
|
-
:extra-params="extraParams"
|
|
91
|
-
:extra-query="extraQuery"
|
|
92
|
-
:show-table-header="showTableHeader"
|
|
93
|
-
:data-table-components="dataTableComponents"
|
|
94
|
-
@sortColumn="sortColumn"
|
|
95
|
-
@actionButtonClicked="actionButtonClicked"
|
|
96
|
-
@actionClicked="actionClicked" />
|
|
97
|
-
<SelectPaginationComponent
|
|
98
|
-
:meta="pagination.meta"
|
|
99
|
-
:links="pagination.links"
|
|
100
|
-
@updatePage="updatePage" />
|
|
101
|
-
</div>
|
|
102
|
-
</div>
|
|
103
|
-
</div>
|
|
104
|
-
</template>
|
|
105
|
-
|
|
106
|
-
<script setup>
|
|
107
|
-
|
|
108
|
-
import { markRaw, onMounted, reactive, ref, watch } from 'vue'
|
|
109
|
-
import axios from 'axios'
|
|
110
|
-
|
|
111
|
-
import NavDropdownComponent from './components/NavDropdownComponent.vue'
|
|
112
|
-
import IconRouteComponent from './components/IconRouteComponent.vue'
|
|
113
|
-
import IconLinkComponent from './components/IconLinkComponent.vue'
|
|
114
|
-
import DisabledLinkComponent from './components/DisabledLinkComponent.vue'
|
|
115
|
-
import DataTableComponent from './components/DataTableComponent.vue'
|
|
116
|
-
import SelectPaginationComponent from './components/SelectPaginationComponent.vue'
|
|
117
|
-
|
|
118
|
-
const props = defineProps({
|
|
119
|
-
dataUrl: {
|
|
120
|
-
type: String,
|
|
121
|
-
required: true
|
|
122
|
-
},
|
|
123
|
-
dataMethod: {
|
|
124
|
-
type: String,
|
|
125
|
-
default: 'post',
|
|
126
|
-
},
|
|
127
|
-
model: {
|
|
128
|
-
type: Object,
|
|
129
|
-
required: true
|
|
130
|
-
},
|
|
131
|
-
policyUrl: {
|
|
132
|
-
type: String,
|
|
133
|
-
required: true
|
|
134
|
-
},
|
|
135
|
-
policyMethod: {
|
|
136
|
-
type: String,
|
|
137
|
-
default: 'post'
|
|
138
|
-
},
|
|
139
|
-
showTopbar:{
|
|
140
|
-
type: Boolean,
|
|
141
|
-
default: true
|
|
142
|
-
},
|
|
143
|
-
hasActions: {
|
|
144
|
-
type: Boolean,
|
|
145
|
-
default: true,
|
|
146
|
-
},
|
|
147
|
-
hasFilter: {
|
|
148
|
-
type: Boolean,
|
|
149
|
-
default: true,
|
|
150
|
-
},
|
|
151
|
-
// Vue 3 exige factoria en los defaults de objeto y array.
|
|
152
|
-
formFilters: {
|
|
153
|
-
type: Object,
|
|
154
|
-
default: () => ({})
|
|
155
|
-
},
|
|
156
|
-
externalFilters: {
|
|
157
|
-
type: Object,
|
|
158
|
-
default: () => ({})
|
|
159
|
-
},
|
|
160
|
-
extraParams: {
|
|
161
|
-
type: Object,
|
|
162
|
-
default: () => ({})
|
|
163
|
-
},
|
|
164
|
-
extraQuery: {
|
|
165
|
-
type: Object,
|
|
166
|
-
default: () => ({})
|
|
167
|
-
},
|
|
168
|
-
hideColumns: {
|
|
169
|
-
type: Array,
|
|
170
|
-
default: () => []
|
|
171
|
-
},
|
|
172
|
-
cardWrapper: {
|
|
173
|
-
type: Boolean,
|
|
174
|
-
default: true
|
|
175
|
-
},
|
|
176
|
-
showTableHeader: {
|
|
177
|
-
type: Boolean,
|
|
178
|
-
default: true,
|
|
179
|
-
}
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Se leia de la global `csrf_token`, que la aplicacion anfitriona tenia
|
|
184
|
-
* que definir en window: el componente no se podia montar fuera de ella.
|
|
185
|
-
*/
|
|
186
|
-
const csrfToken = () => globalThis.csrf_token
|
|
187
|
-
?? document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
|
|
188
|
-
?? ''
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Sustituye a `_.isEqual` de lodash, que se usaba como global sin
|
|
192
|
-
* declararla como dependencia.
|
|
193
|
-
*/
|
|
194
|
-
const isEqual = (a, b) => {
|
|
195
|
-
|
|
196
|
-
if (a === b) {
|
|
197
|
-
return true
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) {
|
|
201
|
-
return false
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const keysA = Object.keys(a)
|
|
205
|
-
const keysB = Object.keys(b)
|
|
206
|
-
|
|
207
|
-
return keysA.length === keysB.length && keysA.every((key) => isEqual(a[key], b[key]))
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const hiddenColumnIds = () => props.hideColumns.map(
|
|
212
|
-
// Se admite tanto ['name'] como [{ id: 'name' }]: el contrato nunca
|
|
213
|
-
// estuvo documentado y por ahi circulan las dos formas.
|
|
214
|
-
(column) => typeof column === 'string' ? column : column?.id
|
|
215
|
-
)
|
|
216
|
-
|
|
217
|
-
const buildHead = () => {
|
|
218
|
-
|
|
219
|
-
const hidden = hiddenColumnIds()
|
|
220
|
-
|
|
221
|
-
return props.model.dataTableHead().filter((column) => ! hidden.includes(column.id))
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const registerComponents = () => {
|
|
226
|
-
|
|
227
|
-
if (typeof props.model.dataTableComponents !== 'function') {
|
|
228
|
-
return {}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const declared = props.model.dataTableComponents()
|
|
232
|
-
|
|
233
|
-
return Object.keys(declared).reduce((components, key) => {
|
|
234
|
-
components[key] = markRaw(declared[key])
|
|
235
|
-
|
|
236
|
-
return components
|
|
237
|
-
}, {})
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const crudActions = ref(props.model.crudActions())
|
|
242
|
-
|
|
243
|
-
const dataTable = reactive({
|
|
244
|
-
head: buildHead(),
|
|
245
|
-
body: [],
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
const pagination = reactive({
|
|
249
|
-
meta: [],
|
|
250
|
-
links: [],
|
|
251
|
-
})
|
|
252
|
-
|
|
253
|
-
const dataTableComponents = registerComponents()
|
|
254
|
-
|
|
255
|
-
const sort = reactive(props.model.dataTableSort())
|
|
256
|
-
|
|
257
|
-
const orderBy = ref('id')
|
|
258
|
-
const internalSort = ref(false)
|
|
259
|
-
const page = ref(1)
|
|
260
|
-
|
|
261
|
-
let fetchDataAttempts = 0
|
|
262
|
-
let fetchPoliciesAttempts = 0
|
|
263
|
-
|
|
264
|
-
const getFilters = () => {
|
|
265
|
-
|
|
266
|
-
const params = {
|
|
267
|
-
_token: csrfToken(),
|
|
268
|
-
managed: true,
|
|
269
|
-
except_view_any: true,
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const order = {
|
|
273
|
-
orderBy: orderBy.value,
|
|
274
|
-
orderMode: sort[orderBy.value],
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Con orden interno, el del usuario gana a lo que traigan los filtros
|
|
278
|
-
// externos; sin el, es al reves.
|
|
279
|
-
return internalSort.value
|
|
280
|
-
? { ...params, ...props.formFilters, ...props.externalFilters, ...order, page: page.value }
|
|
281
|
-
: { ...params, ...props.formFilters, ...order, ...props.externalFilters, page: page.value }
|
|
282
|
-
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const fetchData = () => {
|
|
286
2
|
|
|
287
|
-
|
|
3
|
+
<section :class="[theme.datatable, cardWrapper ? theme.surface : null]">
|
|
4
|
+
|
|
5
|
+
<!-- Con filas seleccionadas, la barra dice cuántas y qué hacer con ellas,
|
|
6
|
+
en el mismo sitio que la de siempre para que la tabla no salte. -->
|
|
7
|
+
<div
|
|
8
|
+
v-if="bulkVisible"
|
|
9
|
+
:class="theme.bulkBar"
|
|
10
|
+
role="region"
|
|
11
|
+
:aria-label="text.selection">
|
|
12
|
+
<span :class="theme.bulkCount" aria-live="polite">{{ selectedIds.length }} {{ text.selected }}</span>
|
|
13
|
+
<button
|
|
14
|
+
v-for="action in bulkActions"
|
|
15
|
+
:key="action.id ?? action.name"
|
|
16
|
+
type="button"
|
|
17
|
+
:class="[action.danger ? theme.buttonDanger : theme.buttonSecondary, 'fe-button-sm']"
|
|
18
|
+
@click="runBulk(action)">
|
|
19
|
+
<IconComponent v-if="action.icon" :name="action.icon" :size="14" />
|
|
20
|
+
<span>{{ action.name }}</span>
|
|
21
|
+
</button>
|
|
22
|
+
<span :class="theme.toolbarSpacer" />
|
|
23
|
+
<button type="button" :class="theme.buttonLink" @click="clearSelection">{{ text.clearSelection }}</button>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div v-else-if="showTopbar" :class="theme.toolbar">
|
|
27
|
+
<MenuComponent
|
|
28
|
+
v-if="hasActions"
|
|
29
|
+
:items="crudItems"
|
|
30
|
+
:label="text.actions"
|
|
31
|
+
placement="bottom-start"
|
|
32
|
+
:before-open="prepareCrud">
|
|
33
|
+
<template #trigger="{ toggle, loading: busy, triggerProps }">
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
:class="theme.buttonSecondary"
|
|
37
|
+
v-bind="triggerProps"
|
|
38
|
+
:disabled="busy"
|
|
39
|
+
@click="toggle">
|
|
40
|
+
<IconComponent name="actions" :size="14" />
|
|
41
|
+
<span>{{ text.actions }}</span>
|
|
42
|
+
</button>
|
|
43
|
+
</template>
|
|
44
|
+
</MenuComponent>
|
|
45
|
+
|
|
46
|
+
<span :class="theme.toolbarSpacer" />
|
|
47
|
+
|
|
48
|
+
<button type="button" :class="theme.iconButton" :aria-label="text.refresh" @click="refresh">
|
|
49
|
+
<IconComponent name="refresh" :size="16" />
|
|
50
|
+
</button>
|
|
51
|
+
|
|
52
|
+
<button
|
|
53
|
+
v-if="hasFilter"
|
|
54
|
+
type="button"
|
|
55
|
+
:class="theme.iconButton"
|
|
56
|
+
:aria-label="text.filters"
|
|
57
|
+
:aria-expanded="filtersOpen ? 'true' : 'false'"
|
|
58
|
+
:aria-controls="filtersId"
|
|
59
|
+
@click="filtersOpen = ! filtersOpen">
|
|
60
|
+
<IconComponent name="filter" :size="16" />
|
|
61
|
+
</button>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<!-- Abrir el panel es estado del componente. Antes lo hacía uk-toggle
|
|
65
|
+
buscando `.filter-form` por selector, y Vue y React acabaron
|
|
66
|
+
comportándose distinto. -->
|
|
67
|
+
<div
|
|
68
|
+
v-if="showTopbar && hasFilter"
|
|
69
|
+
:id="filtersId"
|
|
70
|
+
:class="['filter-form', theme.datatableFilters]"
|
|
71
|
+
:hidden="! filtersOpen">
|
|
72
|
+
<slot name="filterForm"></slot>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<DataTableComponent
|
|
76
|
+
:table="table"
|
|
77
|
+
:head="visibleHead"
|
|
78
|
+
:rows="rows"
|
|
79
|
+
:clones="clones"
|
|
80
|
+
:loading="loading"
|
|
81
|
+
:error="error"
|
|
82
|
+
:actions="hasActions"
|
|
83
|
+
:selectable="selectable"
|
|
84
|
+
:show-table-header="showTableHeader"
|
|
85
|
+
:data-table-components="dataTableComponents"
|
|
86
|
+
:labels="text"
|
|
87
|
+
:order-by="orderBy"
|
|
88
|
+
:sort="sort"
|
|
89
|
+
:items-for="rowItems"
|
|
90
|
+
:prepare-row="preparePolicies"
|
|
91
|
+
@sort-column="sortColumn"
|
|
92
|
+
@retry="refresh" />
|
|
93
|
+
|
|
94
|
+
<!-- Con un error y sin filas, un «No hay resultados» en el pie
|
|
95
|
+
contradiría el motivo que ya da la tabla. -->
|
|
96
|
+
<SelectPaginationComponent
|
|
97
|
+
v-if="! (error && rows.length === 0)"
|
|
98
|
+
:meta="meta"
|
|
99
|
+
:links="links"
|
|
100
|
+
:labels="text"
|
|
101
|
+
@update-page="updatePage" />
|
|
102
|
+
|
|
103
|
+
</section>
|
|
288
104
|
|
|
289
|
-
|
|
290
|
-
method: props.dataMethod,
|
|
291
|
-
url: props.dataUrl,
|
|
292
|
-
data: props.dataMethod === 'post' ? filters : null,
|
|
293
|
-
params: props.dataMethod === 'get' ? filters : null,
|
|
294
|
-
}).then((res) => {
|
|
295
|
-
|
|
296
|
-
fetchDataAttempts = 0
|
|
297
|
-
dataTable.body = res.data.data
|
|
298
|
-
pagination.meta = res.data.meta
|
|
299
|
-
pagination.links = res.data.links
|
|
300
|
-
|
|
301
|
-
}).catch((error) => {
|
|
302
|
-
|
|
303
|
-
// Un fallo de red no trae respuesta: leer error.response.status
|
|
304
|
-
// sin comprobarlo lanzaba un TypeError dentro del propio manejador.
|
|
305
|
-
if (error.response?.status === 403) {
|
|
306
|
-
return
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (fetchDataAttempts <= 3) {
|
|
310
|
-
setTimeout(() => {
|
|
311
|
-
++fetchDataAttempts
|
|
312
|
-
fetchData()
|
|
313
|
-
}, 1500)
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
})
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const updateFilters = () => {
|
|
321
|
-
props.model.setFilters(getFilters())
|
|
322
|
-
|
|
323
|
-
return fetchData()
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const sortColumn = (column) => {
|
|
327
|
-
|
|
328
|
-
if (column.sortable !== true) {
|
|
329
|
-
return
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// A partir de aqui el orden elegido por el usuario manda sobre el que
|
|
333
|
-
// puedan traer los filtros externos.
|
|
334
|
-
internalSort.value = true
|
|
335
|
-
orderBy.value = column.id
|
|
336
|
-
sort[column.id] = sort[column.id] === 'asc' ? 'desc' : 'asc'
|
|
337
|
-
|
|
338
|
-
updateFilters()
|
|
339
|
-
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const updatePage = (newPage) => {
|
|
343
|
-
page.value = newPage
|
|
344
|
-
|
|
345
|
-
updateFilters()
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const actionClicked = (action) => {
|
|
349
|
-
return props.model[action.callback](action.params)
|
|
350
|
-
.then(() => updateFilters())
|
|
351
|
-
.catch((error) => console.error(error))
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
const actionButtonClicked = (actions) => {
|
|
355
|
-
|
|
356
|
-
const requestData = {
|
|
357
|
-
_token: csrfToken(),
|
|
358
|
-
id: actions[0]?.params?.id ?? null,
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
return axios({
|
|
362
|
-
method: props.policyMethod,
|
|
363
|
-
url: props.policyUrl,
|
|
364
|
-
data: props.policyMethod === 'post' ? requestData : null,
|
|
365
|
-
params: props.policyMethod === 'get' ? requestData : null,
|
|
366
|
-
}).then((res) => {
|
|
367
|
-
|
|
368
|
-
fetchPoliciesAttempts = 0
|
|
369
|
-
|
|
370
|
-
actions.forEach((action) => {
|
|
371
|
-
if (res.data[action.id]) {
|
|
372
|
-
action.policy = true
|
|
373
|
-
}
|
|
374
|
-
})
|
|
375
|
-
|
|
376
|
-
}).catch(() => {
|
|
377
|
-
|
|
378
|
-
if (fetchPoliciesAttempts <= 3) {
|
|
379
|
-
setTimeout(() => {
|
|
380
|
-
++fetchPoliciesAttempts
|
|
381
|
-
actionButtonClicked(actions)
|
|
382
|
-
}, 1500)
|
|
383
|
-
|
|
384
|
-
return
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
setTimeout(() => {
|
|
388
|
-
fetchPoliciesAttempts = 0
|
|
389
|
-
}, 3000)
|
|
390
|
-
|
|
391
|
-
})
|
|
392
|
-
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
const closeDropdown = (event) => {
|
|
396
|
-
|
|
397
|
-
const dropdown = event.target.closest('.uk-dropdown')
|
|
398
|
-
|
|
399
|
-
if (dropdown) {
|
|
400
|
-
globalThis.UIkit?.dropdown(dropdown)?.hide(false)
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
watch(() => props.formFilters, () => {
|
|
406
|
-
page.value = 1
|
|
407
|
-
updateFilters()
|
|
408
|
-
}, { deep: true })
|
|
409
|
-
|
|
410
|
-
watch(() => props.externalFilters, (value, previous) => {
|
|
411
|
-
if (! isEqual(value, previous)) {
|
|
412
|
-
updateFilters()
|
|
413
|
-
}
|
|
414
|
-
}, { deep: true })
|
|
105
|
+
</template>
|
|
415
106
|
|
|
416
|
-
|
|
107
|
+
<script setup>
|
|
417
108
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Un listado sobre el contrato del modelo: `crudActions()`,
|
|
111
|
+
* `dataTableHead()`, `dataTableSort()`, `setFilters()` y, si las hay,
|
|
112
|
+
* `bulkActions()` y `dataTableComponents()`.
|
|
113
|
+
*
|
|
114
|
+
* El modelo es el mismo archivo para Vue y para React, y esta tabla recibe
|
|
115
|
+
* exactamente los mismos props que su gemela.
|
|
116
|
+
*/
|
|
117
|
+
|
|
118
|
+
import { computed, inject, markRaw, ref, useId } from 'vue'
|
|
119
|
+
import { routerKey } from 'vue-router'
|
|
120
|
+
import IconComponent from 'innoboxrr-form-elements/src/IconComponent.vue'
|
|
121
|
+
import MenuComponent from 'innoboxrr-form-elements/src/MenuComponent.vue'
|
|
122
|
+
|
|
123
|
+
import DataTableComponent from './components/DataTableComponent.vue'
|
|
124
|
+
import SelectPaginationComponent from './components/SelectPaginationComponent.vue'
|
|
125
|
+
import useDataTable from './useDataTable.js'
|
|
126
|
+
import useTheme from './useTheme.js'
|
|
127
|
+
import { DEFAULT_LABELS, menuItems } from './table.js'
|
|
128
|
+
|
|
129
|
+
const props = defineProps({
|
|
130
|
+
dataUrl: {
|
|
131
|
+
type: String,
|
|
132
|
+
required: true,
|
|
133
|
+
},
|
|
134
|
+
dataMethod: {
|
|
135
|
+
type: String,
|
|
136
|
+
default: 'post',
|
|
137
|
+
},
|
|
138
|
+
model: {
|
|
139
|
+
type: Object,
|
|
140
|
+
required: true,
|
|
141
|
+
},
|
|
142
|
+
policyUrl: {
|
|
143
|
+
type: String,
|
|
144
|
+
required: true,
|
|
145
|
+
},
|
|
146
|
+
policyMethod: {
|
|
147
|
+
type: String,
|
|
148
|
+
default: 'post',
|
|
149
|
+
},
|
|
150
|
+
showTopbar: {
|
|
151
|
+
type: Boolean,
|
|
152
|
+
default: true,
|
|
153
|
+
},
|
|
154
|
+
hasActions: {
|
|
155
|
+
type: Boolean,
|
|
156
|
+
default: true,
|
|
157
|
+
},
|
|
158
|
+
hasFilter: {
|
|
159
|
+
type: Boolean,
|
|
160
|
+
default: true,
|
|
161
|
+
},
|
|
162
|
+
// Vue 3 exige factoría en los defaults de objeto y array.
|
|
163
|
+
formFilters: {
|
|
164
|
+
type: Object,
|
|
165
|
+
default: () => ({}),
|
|
166
|
+
},
|
|
167
|
+
externalFilters: {
|
|
168
|
+
type: Object,
|
|
169
|
+
default: () => ({}),
|
|
170
|
+
},
|
|
171
|
+
extraParams: {
|
|
172
|
+
type: Object,
|
|
173
|
+
default: () => ({}),
|
|
174
|
+
},
|
|
175
|
+
extraQuery: {
|
|
176
|
+
type: Object,
|
|
177
|
+
default: () => ({}),
|
|
178
|
+
},
|
|
179
|
+
hideColumns: {
|
|
180
|
+
type: Array,
|
|
181
|
+
default: () => [],
|
|
182
|
+
},
|
|
183
|
+
cardWrapper: {
|
|
184
|
+
type: Boolean,
|
|
185
|
+
default: true,
|
|
186
|
+
},
|
|
187
|
+
showTableHeader: {
|
|
188
|
+
type: Boolean,
|
|
189
|
+
default: true,
|
|
190
|
+
},
|
|
191
|
+
// Casillas para elegir filas y la barra de acciones masivas.
|
|
192
|
+
selectable: {
|
|
193
|
+
type: Boolean,
|
|
194
|
+
default: false,
|
|
195
|
+
},
|
|
196
|
+
// Los textos de la tabla; se mezclan con los de fábrica.
|
|
197
|
+
labels: {
|
|
198
|
+
type: Object,
|
|
199
|
+
default: () => ({}),
|
|
200
|
+
},
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
// Sin router montado no hay a dónde navegar, pero la tabla se puede
|
|
204
|
+
// pintar igual: inject con valor por defecto no avisa.
|
|
205
|
+
const router = inject(routerKey, null)
|
|
206
|
+
|
|
207
|
+
const navigate = (target) => router?.push(target)
|
|
208
|
+
|
|
209
|
+
const theme = useTheme()
|
|
210
|
+
|
|
211
|
+
const text = computed(() => ({ ...DEFAULT_LABELS, ...props.labels }))
|
|
212
|
+
|
|
213
|
+
const filtersOpen = ref(false)
|
|
214
|
+
const filtersId = useId()
|
|
215
|
+
|
|
216
|
+
const {
|
|
217
|
+
table,
|
|
218
|
+
visibleHead,
|
|
219
|
+
rows,
|
|
220
|
+
clones,
|
|
221
|
+
meta,
|
|
222
|
+
links,
|
|
223
|
+
loading,
|
|
224
|
+
error,
|
|
225
|
+
sort,
|
|
226
|
+
orderBy,
|
|
227
|
+
crudActions,
|
|
228
|
+
bulkActions,
|
|
229
|
+
rowActions,
|
|
230
|
+
selectedIds,
|
|
231
|
+
dataTable,
|
|
232
|
+
pagination,
|
|
233
|
+
refresh,
|
|
234
|
+
clearSelection,
|
|
235
|
+
sortColumn,
|
|
236
|
+
updatePage,
|
|
237
|
+
preparePolicies,
|
|
238
|
+
run,
|
|
239
|
+
runBulk,
|
|
240
|
+
} = useDataTable(props, { navigate: router ? navigate : null, labels: text })
|
|
241
|
+
|
|
242
|
+
const dataTableComponents = typeof props.model.dataTableComponents === 'function'
|
|
243
|
+
? Object.fromEntries(Object.entries(props.model.dataTableComponents()).map(([key, component]) => [key, markRaw(component)]))
|
|
244
|
+
: {}
|
|
245
|
+
|
|
246
|
+
const bulkVisible = computed(() => props.selectable && selectedIds.value.length > 0)
|
|
247
|
+
|
|
248
|
+
const crudItems = computed(() => menuItems(crudActions.value, text.value, run))
|
|
249
|
+
|
|
250
|
+
const rowItems = (row) => menuItems(rowActions(row), text.value, run)
|
|
251
|
+
|
|
252
|
+
const prepareCrud = () => preparePolicies(null)
|
|
253
|
+
|
|
254
|
+
defineExpose({ refresh, clearSelection, table, selectedIds, crudActions, dataTable, pagination })
|
|
421
255
|
|
|
422
256
|
</script>
|
|
423
|
-
|
|
424
|
-
<style scoped>
|
|
425
|
-
.tools-icon {
|
|
426
|
-
font-size: 30px;
|
|
427
|
-
color: var(--secondary-color);
|
|
428
|
-
padding: 10px 15px;
|
|
429
|
-
}
|
|
430
|
-
</style>
|