@groupteknology/vuno 0.4.0 → 0.6.1

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 (81) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +198 -0
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +6 -0
  5. package/dist/runtime/app/assets/css/main.css +1 -1
  6. package/dist/runtime/app/components/App/App.d.vue.ts +33 -0
  7. package/dist/runtime/app/components/App/App.vue +127 -0
  8. package/dist/runtime/app/components/App/App.vue.d.ts +33 -0
  9. package/dist/runtime/app/components/App/AppContext.d.vue.ts +3 -0
  10. package/dist/runtime/app/components/App/AppContext.vue +7 -0
  11. package/dist/runtime/app/components/App/AppContext.vue.d.ts +3 -0
  12. package/dist/runtime/app/components/App/AppModal.d.vue.ts +38 -0
  13. package/dist/runtime/app/components/App/AppModal.vue +103 -0
  14. package/dist/runtime/app/components/App/AppModal.vue.d.ts +38 -0
  15. package/dist/runtime/app/components/App/AppOverlay.d.vue.ts +7 -0
  16. package/dist/runtime/app/components/App/AppOverlay.vue +33 -0
  17. package/dist/runtime/app/components/App/AppOverlay.vue.d.ts +7 -0
  18. package/dist/runtime/app/components/App/AppToast.d.vue.ts +11 -0
  19. package/dist/runtime/app/components/App/AppToast.vue +53 -0
  20. package/dist/runtime/app/components/App/AppToast.vue.d.ts +11 -0
  21. package/dist/runtime/app/components/App/AppToaster.d.vue.ts +11 -0
  22. package/dist/runtime/app/components/App/AppToaster.vue +49 -0
  23. package/dist/runtime/app/components/App/AppToaster.vue.d.ts +11 -0
  24. package/dist/runtime/app/components/Cell/CellBadge.d.vue.ts +5 -0
  25. package/dist/runtime/app/components/Cell/CellBadge.vue +24 -0
  26. package/dist/runtime/app/components/Cell/CellBadge.vue.d.ts +5 -0
  27. package/dist/runtime/app/components/Cell/CellBoolean.d.vue.ts +5 -0
  28. package/dist/runtime/app/components/Cell/CellBoolean.vue +21 -0
  29. package/dist/runtime/app/components/Cell/CellBoolean.vue.d.ts +5 -0
  30. package/dist/runtime/app/components/Cell/CellCurrency.d.vue.ts +5 -0
  31. package/dist/runtime/app/components/Cell/CellCurrency.vue +14 -0
  32. package/dist/runtime/app/components/Cell/CellCurrency.vue.d.ts +5 -0
  33. package/dist/runtime/app/components/Cell/CellDate.d.vue.ts +5 -0
  34. package/dist/runtime/app/components/Cell/CellDate.vue +19 -0
  35. package/dist/runtime/app/components/Cell/CellDate.vue.d.ts +5 -0
  36. package/dist/runtime/app/components/Cell/CellImage.d.vue.ts +5 -0
  37. package/dist/runtime/app/components/Cell/CellImage.vue +22 -0
  38. package/dist/runtime/app/components/Cell/CellImage.vue.d.ts +5 -0
  39. package/dist/runtime/app/components/Cell/CellNumber.d.vue.ts +5 -0
  40. package/dist/runtime/app/components/Cell/CellNumber.vue +15 -0
  41. package/dist/runtime/app/components/Cell/CellNumber.vue.d.ts +5 -0
  42. package/dist/runtime/app/components/Cell/CellText.d.vue.ts +5 -0
  43. package/dist/runtime/app/components/Cell/CellText.vue +13 -0
  44. package/dist/runtime/app/components/Cell/CellText.vue.d.ts +5 -0
  45. package/dist/runtime/app/components/Confirm/Confirm.vue +20 -13
  46. package/dist/runtime/app/components/Field/FieldArray.vue +68 -68
  47. package/dist/runtime/app/components/Field/FieldColor.vue +36 -36
  48. package/dist/runtime/app/components/Form/Form.vue +61 -49
  49. package/dist/runtime/app/components/Form/FormModal.vue +23 -15
  50. package/dist/runtime/app/components/Form/FormSection.vue +27 -27
  51. package/dist/runtime/app/components/Page/Page.vue +46 -46
  52. package/dist/runtime/app/components/State/EmptyState.vue +20 -20
  53. package/dist/runtime/app/components/State/FailureState.vue +21 -21
  54. package/dist/runtime/app/components/Table/Table.vue +286 -373
  55. package/dist/runtime/app/components/Table/TableCell.vue +24 -73
  56. package/dist/runtime/app/composables/useVunoConfirm.d.ts +15 -0
  57. package/dist/runtime/app/composables/useVunoConfirm.js +5 -0
  58. package/dist/runtime/app/composables/useVunoContext.d.ts +13 -0
  59. package/dist/runtime/app/composables/useVunoContext.js +9 -0
  60. package/dist/runtime/app/composables/useVunoModal.d.ts +21 -0
  61. package/dist/runtime/app/composables/useVunoModal.js +26 -0
  62. package/dist/runtime/app/composables/useVunoOverlay.d.ts +22 -0
  63. package/dist/runtime/app/composables/useVunoOverlay.js +10 -0
  64. package/dist/runtime/app/composables/useVunoTableQuery.js +3 -3
  65. package/dist/runtime/app/composables/useVunoTheme.js +2 -1
  66. package/dist/runtime/app/composables/useVunoToast.d.ts +28 -0
  67. package/dist/runtime/app/composables/useVunoToast.js +13 -0
  68. package/dist/runtime/app/internal/useVunoTable.d.ts +65 -0
  69. package/dist/runtime/app/internal/useVunoTable.js +161 -0
  70. package/dist/runtime/app/theme/index.js +69 -2
  71. package/dist/runtime/app/types/app.d.ts +83 -0
  72. package/dist/runtime/app/types/app.js +0 -0
  73. package/dist/runtime/app/types/confirm.d.ts +8 -0
  74. package/dist/runtime/app/types/index.d.ts +1 -0
  75. package/dist/runtime/app/types/table.d.ts +80 -13
  76. package/dist/runtime/app/types/theme.d.ts +12 -0
  77. package/dist/runtime/app/utils/column.d.ts +9 -0
  78. package/dist/runtime/app/utils/column.js +5 -0
  79. package/dist/runtime/app/utils/pagination.d.ts +12 -0
  80. package/dist/runtime/app/utils/pagination.js +6 -0
  81. package/package.json +6 -3
@@ -1,10 +1,11 @@
1
1
  <script setup>
2
- import { computed, onBeforeUnmount, ref, useTemplateRef, watch } from "vue";
2
+ import { computed, useTemplateRef } from "vue";
3
3
  import EmptyState from "#vuno/components/State/EmptyState.vue";
4
4
  import FailureState from "#vuno/components/State/FailureState.vue";
5
5
  import Form from "#vuno/components/Form/Form.vue";
6
6
  import { useVunoIcons, useVunoMessages, useVunoTheme } from "#vuno/composables/useVunoTheme";
7
- import { getPath } from "#vuno/utils/path";
7
+ import { useVunoTable } from "#vuno/internal/useVunoTable";
8
+ import { resolveColumnValue } from "#vuno/utils/column";
8
9
  import TableCell from "./TableCell.vue";
9
10
  const props = defineProps({
10
11
  data: { type: Array, required: true },
@@ -23,128 +24,29 @@ defineSlots();
23
24
  const icons = useVunoIcons();
24
25
  const messages = useVunoMessages();
25
26
  const ui = useVunoTheme("table", () => props.ui);
26
- const rowKey = computed(() => props.definition.rowKey ?? "id");
27
- const pageSizes = computed(() => props.definition.pageSizes ?? [10, 25, 50, 100]);
28
- const getRowId = (row) => String(getPath(row, rowKey.value) ?? "");
29
- const search = ref(query.value.search);
30
- let searchTimer;
31
- watch(search, (value) => {
32
- clearTimeout(searchTimer);
33
- searchTimer = setTimeout(() => {
34
- if (value === query.value.search) return;
35
- query.value = { ...query.value, page: 1, search: value };
36
- }, 300);
27
+ const table = useVunoTable({
28
+ query,
29
+ selection,
30
+ definition: () => props.definition,
31
+ total: () => total.value
37
32
  });
38
- watch(
39
- () => query.value.search,
40
- (value) => {
41
- if (value !== search.value) search.value = value;
42
- }
43
- );
44
- onBeforeUnmount(() => clearTimeout(searchTimer));
33
+ const { activeFilters, applyFilters, clearFilters, clearSelection, columns, filtersDefinition, filtersOpen, filterState, getRowId, goToPage, pageSizes, paginated, rangeEnd, rangeStart, removeFilter, rowSelection, search, searchable, setPageSize, sortDirectionFor, toggleSort } = table;
45
34
  const searchInput = useTemplateRef("searchInput");
46
35
  function clearSearch() {
47
- clearTimeout(searchTimer);
48
- search.value = "";
49
- query.value = { ...query.value, page: 1, search: "" };
36
+ table.clearSearch();
50
37
  searchInput.value?.inputRef?.focus();
51
38
  }
52
- function sortDirectionFor(key) {
53
- return query.value.sort?.column === key ? query.value.sort.direction : null;
54
- }
55
- function toggleSort(key) {
56
- const current = sortDirectionFor(key);
57
- const sort = current === null ? { column: key, direction: "asc" } : current === "asc" ? { column: key, direction: "desc" } : null;
58
- query.value = { ...query.value, page: 1, sort };
59
- }
60
39
  function sortIcon(key) {
61
40
  const direction = sortDirectionFor(key);
62
41
  if (direction === "asc") return icons.value.sortAsc;
63
42
  if (direction === "desc") return icons.value.sortDesc;
64
43
  return icons.value.sort;
65
44
  }
66
- const filtersOpen = ref(false);
67
- const filterState = ref({ ...query.value.filters });
68
- watch(
69
- () => query.value.filters,
70
- (value) => {
71
- filterState.value = { ...value };
72
- },
73
- { deep: true }
74
- );
75
- const hasValue = (value) => value !== void 0 && value !== null && value !== "";
76
- const filtersDefinition = computed(() => ({
77
- sections: [{ id: "filters", fields: props.definition.filters ?? [] }]
78
- }));
79
- const activeFilters = computed(
80
- () => (props.definition.filters ?? []).filter((field) => hasValue(query.value.filters[field.name])).map((field) => {
81
- const value = query.value.filters[field.name];
82
- const option = "items" in field ? field.items?.find((item) => String(item.value) === String(value)) : void 0;
83
- return {
84
- name: field.name,
85
- label: field.label ?? field.name,
86
- value: option?.label ?? String(value)
87
- };
88
- })
89
- );
90
- function applyFilters() {
91
- query.value = { ...query.value, filters: { ...filterState.value }, page: 1 };
92
- filtersOpen.value = false;
93
- }
94
- function clearFilters() {
95
- filterState.value = {};
96
- query.value = { ...query.value, filters: {}, page: 1 };
97
- filtersOpen.value = false;
98
- }
99
- function removeFilter(name) {
100
- const filters = { ...query.value.filters };
101
- delete filters[name];
102
- query.value = { ...query.value, filters, page: 1 };
103
- }
104
- const rowSelection = computed({
105
- get: () => Object.fromEntries(selection.value.map((key) => [String(key), true])),
106
- set: (value) => {
107
- selection.value = Object.entries(value).filter(([, selected]) => selected).map(([key]) => key);
108
- }
109
- });
110
- const clearSelection = () => {
111
- selection.value = [];
112
- };
113
- const columns = computed(() => {
114
- const list = [];
115
- if (props.definition.selectable) {
116
- list.push({ id: "select", enableSorting: false });
117
- }
118
- for (const column of props.definition.columns) {
119
- list.push({
120
- id: column.key,
121
- accessorKey: column.key,
122
- enableSorting: false,
123
- // La columna de acciones no lleva rótulo salvo que se pida uno.
124
- header: column.type === "actions" ? column.label ?? "" : column.label ?? column.key,
125
- meta: {
126
- class: {
127
- td: [column.align === "end" ? "text-end" : column.align === "center" ? "text-center" : "", column.class].filter(Boolean).join(" "),
128
- th: [column.align === "end" ? "text-end" : column.align === "center" ? "text-center" : "", column.width].filter(Boolean).join(" ")
129
- }
130
- }
131
- });
132
- }
133
- return list;
134
- });
135
- const rangeStart = computed(() => total.value === 0 ? 0 : (query.value.page - 1) * query.value.pageSize + 1);
136
- const rangeEnd = computed(() => Math.min(query.value.page * query.value.pageSize, total.value));
137
- function onPageChange(page) {
138
- query.value = { ...query.value, page };
139
- }
140
- function onPageSizeChange(size) {
141
- query.value = { ...query.value, page: 1, pageSize: size };
142
- }
143
45
  defineExpose({
144
46
  clearFilters,
145
47
  clearSearch,
146
48
  clearSelection,
147
- goToPage: onPageChange
49
+ goToPage
148
50
  });
149
51
  function onRowSelect(_event, row) {
150
52
  emit("rowClick", row.original);
@@ -152,197 +54,198 @@ function onRowSelect(_event, row) {
152
54
  </script>
153
55
 
154
56
  <template>
155
- <div :class="ui.root">
156
- <div :class="ui.toolbar">
157
- <UInput
158
- ref="searchInput"
159
- v-model="search"
160
- :icon="icons.search"
161
- :placeholder="props.definition.searchPlaceholder ?? messages.searchPlaceholder"
162
- :ui="{ root: ui.search }"
163
- >
164
- <template
165
- v-if="search.length > 0"
166
- #trailing
167
- >
168
- <UButton
169
- :aria-label="messages.clearSearch"
170
- color="neutral"
171
- :icon="icons.clearSearch"
172
- size="xs"
173
- variant="link"
174
- @click="clearSearch"
175
- />
176
- </template>
177
- </UInput>
178
-
179
- <div :class="ui.toolbarActions">
180
- <UButton
181
- v-if="(props.definition.filters?.length ?? 0) > 0"
182
- color="neutral"
183
- :icon="icons.filter"
184
- :label="messages.filters"
185
- variant="outline"
186
- @click="filtersOpen = true"
187
- >
188
- <!-- `UButton` no admite `badge`: el contador va en el slot. -->
189
- <template
190
- v-if="activeFilters.length > 0"
191
- #trailing
192
- >
193
- <UBadge
194
- color="primary"
195
- :label="String(activeFilters.length)"
196
- size="sm"
197
- variant="solid"
198
- />
199
- </template>
200
- </UButton>
201
-
202
- <slot
203
- name="toolbar"
204
- :query="query"
205
- />
206
- </div>
207
- </div>
208
-
209
- <div
210
- v-if="props.definition.selectable && selection.length > 0"
211
- :class="ui.selection"
212
- >
213
- <slot
214
- :clear="clearSelection"
215
- :count="selection.length"
216
- :keys="selection"
217
- name="selection"
218
- >
219
- <span>{{ selection.length }} {{ selection.length === 1 ? messages.rowSelected : messages.rowsSelected }}</span>
220
- <UButton
221
- color="neutral"
222
- :label="messages.clearSelection"
223
- size="xs"
224
- variant="ghost"
225
- @click="clearSelection"
226
- />
227
- </slot>
228
- </div>
229
-
230
- <div
231
- v-if="activeFilters.length > 0"
232
- :class="ui.chips"
233
- >
234
- <UBadge
235
- v-for="filter in activeFilters"
236
- :key="filter.name"
237
- color="neutral"
238
- variant="subtle"
239
- >
240
- <span>{{ filter.label }}: {{ filter.value }}</span>
241
- <UButton
242
- :aria-label="`${messages.clearFilters}: ${filter.label}`"
243
- color="neutral"
244
- :icon="icons.fileRemove"
245
- size="xs"
246
- variant="link"
247
- @click="removeFilter(filter.name)"
248
- />
249
- </UBadge>
250
-
251
- <UButton
252
- color="neutral"
253
- :label="messages.clearFilters"
254
- size="xs"
255
- variant="ghost"
256
- @click="clearFilters"
257
- />
258
- </div>
259
-
260
- <USlideover
261
- v-model:open="filtersOpen"
262
- :title="messages.filtersTitle"
263
- >
264
- <template #body>
265
- <Form
266
- v-model:state="filterState"
267
- :definition="filtersDefinition"
268
- :ui="{ sections: ui.filtersBody }"
269
- />
270
- </template>
271
-
272
- <template #footer>
273
- <div :class="ui.filtersFooter">
274
- <UButton
275
- color="neutral"
276
- :label="messages.clearFilters"
277
- variant="ghost"
278
- @click="clearFilters"
279
- />
280
- <UButton
281
- :label="messages.applyFilters"
282
- @click="applyFilters"
283
- />
284
- </div>
285
- </template>
286
- </USlideover>
287
-
288
- <UTable
289
- v-model:rowSelection="rowSelection"
290
- :columns="columns"
291
- :data="hasFailure ? [] : props.data"
292
- :getRowId="getRowId"
293
- :loading="props.loading ?? false"
294
- :paginationOptions="{ manualPagination: true }"
295
- :sortingOptions="{ manualSorting: true }"
296
- :ui="{ base: ui.table, tbody: ui.tbody, td: ui.td, th: ui.th }"
297
- @select="onRowSelect"
298
- >
299
- <template #select-header>
300
- <UCheckbox
301
- :modelValue="props.data.length > 0 && props.data.every((row2) => selection.includes(getRowId(row2)))"
57
+ <div :class="ui.root">
58
+ <div :class="ui.toolbar">
59
+ <UInput
60
+ v-if="searchable"
61
+ ref="searchInput"
62
+ v-model="search"
63
+ :icon="icons.search"
64
+ :placeholder="props.definition.searchPlaceholder ?? messages.searchPlaceholder"
65
+ :ui="{ root: ui.search }"
66
+ >
67
+ <template
68
+ v-if="search.length > 0"
69
+ #trailing
70
+ >
71
+ <UButton
72
+ :aria-label="messages.clearSearch"
73
+ color="neutral"
74
+ :icon="icons.clearSearch"
75
+ size="xs"
76
+ variant="link"
77
+ @click="clearSearch"
78
+ />
79
+ </template>
80
+ </UInput>
81
+
82
+ <div :class="ui.toolbarActions">
83
+ <UButton
84
+ v-if="(props.definition.filters?.length ?? 0) > 0"
85
+ color="neutral"
86
+ :icon="icons.filter"
87
+ :label="messages.filters"
88
+ variant="outline"
89
+ @click="filtersOpen = true"
90
+ >
91
+ <!-- `UButton` no admite `badge`: el contador va en el slot. -->
92
+ <template
93
+ v-if="activeFilters.length > 0"
94
+ #trailing
95
+ >
96
+ <UBadge
97
+ color="primary"
98
+ :label="String(activeFilters.length)"
99
+ size="sm"
100
+ variant="solid"
101
+ />
102
+ </template>
103
+ </UButton>
104
+
105
+ <slot
106
+ name="toolbar"
107
+ :query="query"
108
+ />
109
+ </div>
110
+ </div>
111
+
112
+ <div
113
+ v-if="props.definition.selectable && selection.length > 0"
114
+ :class="ui.selection"
115
+ >
116
+ <slot
117
+ :clear="clearSelection"
118
+ :count="selection.length"
119
+ :keys="selection"
120
+ name="selection"
121
+ >
122
+ <span>{{ selection.length }} {{ selection.length === 1 ? messages.rowSelected : messages.rowsSelected }}</span>
123
+ <UButton
124
+ color="neutral"
125
+ :label="messages.clearSelection"
126
+ size="xs"
127
+ variant="ghost"
128
+ @click="clearSelection"
129
+ />
130
+ </slot>
131
+ </div>
132
+
133
+ <div
134
+ v-if="activeFilters.length > 0"
135
+ :class="ui.chips"
136
+ >
137
+ <UBadge
138
+ v-for="filter in activeFilters"
139
+ :key="filter.name"
140
+ color="neutral"
141
+ variant="subtle"
142
+ >
143
+ <span>{{ filter.label }}: {{ filter.value }}</span>
144
+ <UButton
145
+ :aria-label="`${messages.clearFilters}: ${filter.label}`"
146
+ color="neutral"
147
+ :icon="icons.fileRemove"
148
+ size="xs"
149
+ variant="link"
150
+ @click="removeFilter(filter.name)"
151
+ />
152
+ </UBadge>
153
+
154
+ <UButton
155
+ color="neutral"
156
+ :label="messages.clearFilters"
157
+ size="xs"
158
+ variant="ghost"
159
+ @click="clearFilters"
160
+ />
161
+ </div>
162
+
163
+ <USlideover
164
+ v-model:open="filtersOpen"
165
+ :title="messages.filtersTitle"
166
+ >
167
+ <template #body>
168
+ <Form
169
+ v-model:state="filterState"
170
+ :definition="filtersDefinition"
171
+ :ui="{ sections: ui.filtersBody }"
172
+ />
173
+ </template>
174
+
175
+ <template #footer>
176
+ <div :class="ui.filtersFooter">
177
+ <UButton
178
+ color="neutral"
179
+ :label="messages.clearFilters"
180
+ variant="ghost"
181
+ @click="clearFilters"
182
+ />
183
+ <UButton
184
+ :label="messages.applyFilters"
185
+ @click="applyFilters"
186
+ />
187
+ </div>
188
+ </template>
189
+ </USlideover>
190
+
191
+ <UTable
192
+ v-model:rowSelection="rowSelection"
193
+ :columns="columns"
194
+ :data="hasFailure ? [] : props.data"
195
+ :getRowId="getRowId"
196
+ :loading="props.loading ?? false"
197
+ :paginationOptions="{ manualPagination: true }"
198
+ :sortingOptions="{ manualSorting: true }"
199
+ :ui="{ base: ui.table, root: ui.tableRoot, tbody: ui.tbody, td: ui.td, th: ui.th }"
200
+ @select="onRowSelect"
201
+ >
202
+ <template #select-header>
203
+ <UCheckbox
204
+ :modelValue="props.data.length > 0 && props.data.every((row2) => selection.includes(getRowId(row2)))"
302
205
  @update:model-value="
303
206
  (checked) => {
304
207
  const keys = props.data.map((row2) => getRowId(row2));
305
208
  selection = checked ? [.../* @__PURE__ */ new Set([...selection, ...keys])] : selection.filter((key) => !keys.includes(String(key)));
306
209
  }
307
- "
308
- />
309
- </template>
310
-
311
- <template #select-cell="{ row }">
312
- <UCheckbox
313
- :modelValue="row.getIsSelected()"
314
- @update:model-value="row.toggleSelected(!row.getIsSelected())"
315
- />
316
- </template>
317
-
318
- <template
319
- v-for="column in props.definition.columns"
320
- :key="`${column.key}-header`"
321
- #[`${column.key}-header`]
322
- >
323
- <UButton
324
- v-if="column.sortable"
325
- class="-mx-2"
326
- color="neutral"
327
- :icon="sortIcon(column.key)"
328
- :label="column.label ?? column.key"
329
- size="xs"
330
- trailing
331
- variant="ghost"
332
- @click="toggleSort(column.key)"
333
- />
334
- <span v-else>{{ column.type === "actions" ? column.label ?? "" : column.label ?? column.key }}</span>
335
- </template>
336
-
337
- <template
338
- v-for="column in props.definition.columns"
339
- :key="`${column.key}-cell`"
340
- #[`${column.key}-cell`]="{ row }"
341
- >
342
- <!-- `stop`: sin esto, abrir el menú dispara también el clic de
343
- fila y se acaban abriendo dos diálogos a la vez. -->
344
- <UDropdownMenu
345
- v-if="column.type === 'actions'"
210
+ "
211
+ />
212
+ </template>
213
+
214
+ <template #select-cell="{ row }">
215
+ <UCheckbox
216
+ :modelValue="row.getIsSelected()"
217
+ @update:model-value="row.toggleSelected(!row.getIsSelected())"
218
+ />
219
+ </template>
220
+
221
+ <template
222
+ v-for="column in props.definition.columns"
223
+ :key="`${column.key}-header`"
224
+ #[`${column.key}-header`]
225
+ >
226
+ <UButton
227
+ v-if="column.sortable"
228
+ class="-mx-2"
229
+ color="neutral"
230
+ :icon="sortIcon(column.key)"
231
+ :label="column.label ?? column.key"
232
+ size="xs"
233
+ trailing
234
+ variant="ghost"
235
+ @click="toggleSort(column.key)"
236
+ />
237
+ <span v-else>{{ column.type === "actions" ? column.label ?? "" : column.label ?? column.key }}</span>
238
+ </template>
239
+
240
+ <template
241
+ v-for="column in props.definition.columns"
242
+ :key="`${column.key}-cell`"
243
+ #[`${column.key}-cell`]="{ row }"
244
+ >
245
+ <!-- `stop`: sin esto, abrir el menú dispara también el clic de
246
+ fila y se acaban abriendo dos diálogos a la vez. -->
247
+ <UDropdownMenu
248
+ v-if="column.type === 'actions'"
346
249
  :items="[
347
250
  column.actions.filter((action) => !action.hidden?.(row.original)).map((action) => ({
348
251
  color: action.color,
@@ -351,82 +254,92 @@ function onRowSelect(_event, row) {
351
254
  label: action.label,
352
255
  onSelect: () => action.onSelect(row.original)
353
256
  }))
354
- ]"
355
- @click.stop
356
- >
357
- <UButton
358
- :aria-label="messages.rowActions"
359
- color="neutral"
360
- :icon="icons.actions"
361
- size="xs"
362
- variant="ghost"
363
- />
364
- </UDropdownMenu>
365
-
366
- <slot
367
- v-else
368
- :name="`cell-${column.key}`"
369
- :row="row.original"
370
- :value="getPath(row.original, column.key)"
371
- >
372
- <TableCell
373
- :column="column"
374
- :row="row.original"
375
- />
376
- </slot>
377
- </template>
378
-
379
- <template #empty>
380
- <!-- Un fallo y una lista vacía no son lo mismo: el primero
381
- ofrece reintentar, el segundo invita a crear el primero. -->
382
- <FailureState
383
- v-if="props.failure"
384
- :failure="props.failure"
385
- />
386
-
387
- <!-- Cargando y sin filas todavía no es «no hay nada»: decirlo
388
- antes de tiempo hace parpadear la tabla en cada cambio. -->
389
- <div v-else-if="props.loading" />
390
-
391
- <slot
392
- v-else
393
- name="empty"
394
- >
395
- <EmptyState
396
- :hint="props.definition.empty?.hint"
397
- :icon="props.definition.empty?.icon"
398
- :title="props.definition.empty?.title"
399
- >
400
- <template #actions>
401
- <slot name="empty-actions" />
402
- </template>
403
- </EmptyState>
404
- </slot>
405
- </template>
406
- </UTable>
407
-
408
- <div
409
- v-if="total > 0 && !hasFailure"
410
- :class="ui.footer"
411
- >
412
- <div :class="ui.meta">
413
- <span>{{ messages.showing }} {{ rangeStart }}–{{ rangeEnd }} {{ messages.of }} {{ total }}</span>
414
-
415
- <USelect
416
- :items="pageSizes"
417
- :modelValue="query.pageSize"
418
- size="xs"
419
- :ui="{ base: 'w-20' }"
420
- @update:model-value="(size) => onPageSizeChange(Number(size))"
421
- />
422
- </div>
423
-
424
- <UPagination
425
- :itemsPerPage="query.pageSize"
426
- :page="query.page"
427
- :total="total"
428
- @update:page="onPageChange"
429
- />
430
- </div>
431
- </div>
257
+ ]"
258
+ @click.stop
259
+ >
260
+ <UButton
261
+ :aria-label="messages.rowActions"
262
+ color="neutral"
263
+ :icon="icons.actions"
264
+ size="xs"
265
+ variant="ghost"
266
+ />
267
+ </UDropdownMenu>
268
+
269
+ <slot
270
+ v-else
271
+ :name="`cell-${column.key}`"
272
+ :row="row.original"
273
+ :value="resolveColumnValue(column, row.original)"
274
+ >
275
+ <TableCell
276
+ :column="column"
277
+ :row="row.original"
278
+ />
279
+ </slot>
280
+ </template>
281
+
282
+ <template #empty>
283
+ <!-- Un fallo y una lista vacía no son lo mismo: el primero
284
+ ofrece reintentar, el segundo invita a crear el primero. -->
285
+ <FailureState
286
+ v-if="props.failure"
287
+ :failure="props.failure"
288
+ />
289
+
290
+ <!-- Cargando y sin filas todavía no es «no hay nada»: decirlo
291
+ antes de tiempo hace parpadear la tabla en cada cambio. -->
292
+ <div v-else-if="props.loading" />
293
+
294
+ <!-- Con `total` por encima de cero y sin filas, lo que falta es
295
+ la página, no los datos. Se corrige sola arriba; entretanto
296
+ no se afirma que no haya nada. -->
297
+ <div v-else-if="total > 0" />
298
+
299
+ <slot
300
+ v-else
301
+ name="empty"
302
+ >
303
+ <EmptyState
304
+ :hint="props.definition.empty?.hint"
305
+ :icon="props.definition.empty?.icon"
306
+ :title="props.definition.empty?.title"
307
+ >
308
+ <template #actions>
309
+ <slot name="empty-actions" />
310
+ </template>
311
+ </EmptyState>
312
+ </slot>
313
+ </template>
314
+ </UTable>
315
+
316
+ <div
317
+ v-if="total > 0 && !hasFailure"
318
+ :class="ui.footer"
319
+ >
320
+ <div :class="ui.meta">
321
+ <span v-if="paginated">{{ messages.showing }} {{ rangeStart }}–{{ rangeEnd }} {{ messages.of }} {{ total }}</span>
322
+
323
+ <!-- Sin paginar, un rango «1–30 de 30» sugiere que hay más. -->
324
+ <span v-else>{{ total }} {{ messages.results }}</span>
325
+
326
+ <USelect
327
+ v-if="paginated"
328
+ :items="pageSizes"
329
+ :modelValue="query.pageSize"
330
+ size="xs"
331
+ :ui="{ base: 'w-20' }"
332
+ @update:model-value="(size) => setPageSize(Number(size))"
333
+ />
334
+ </div>
335
+
336
+ <UPagination
337
+ v-if="paginated"
338
+ :itemsPerPage="query.pageSize"
339
+ :page="query.page"
340
+ :total="total"
341
+ @update:page="goToPage"
342
+ />
343
+ </div>
344
+ </div>
432
345
  </template>