@groupteknology/vuno 0.4.0 → 0.6.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.
Files changed (31) hide show
  1. package/README.md +153 -0
  2. package/dist/module.json +1 -1
  3. package/dist/runtime/app/components/Cell/CellBadge.d.vue.ts +5 -0
  4. package/dist/runtime/app/components/Cell/CellBadge.vue +24 -0
  5. package/dist/runtime/app/components/Cell/CellBadge.vue.d.ts +5 -0
  6. package/dist/runtime/app/components/Cell/CellBoolean.d.vue.ts +5 -0
  7. package/dist/runtime/app/components/Cell/CellBoolean.vue +21 -0
  8. package/dist/runtime/app/components/Cell/CellBoolean.vue.d.ts +5 -0
  9. package/dist/runtime/app/components/Cell/CellCurrency.d.vue.ts +5 -0
  10. package/dist/runtime/app/components/Cell/CellCurrency.vue +14 -0
  11. package/dist/runtime/app/components/Cell/CellCurrency.vue.d.ts +5 -0
  12. package/dist/runtime/app/components/Cell/CellDate.d.vue.ts +5 -0
  13. package/dist/runtime/app/components/Cell/CellDate.vue +19 -0
  14. package/dist/runtime/app/components/Cell/CellDate.vue.d.ts +5 -0
  15. package/dist/runtime/app/components/Cell/CellImage.d.vue.ts +5 -0
  16. package/dist/runtime/app/components/Cell/CellImage.vue +22 -0
  17. package/dist/runtime/app/components/Cell/CellImage.vue.d.ts +5 -0
  18. package/dist/runtime/app/components/Cell/CellNumber.d.vue.ts +5 -0
  19. package/dist/runtime/app/components/Cell/CellNumber.vue +15 -0
  20. package/dist/runtime/app/components/Cell/CellNumber.vue.d.ts +5 -0
  21. package/dist/runtime/app/components/Cell/CellText.d.vue.ts +5 -0
  22. package/dist/runtime/app/components/Cell/CellText.vue +13 -0
  23. package/dist/runtime/app/components/Cell/CellText.vue.d.ts +5 -0
  24. package/dist/runtime/app/components/Table/Table.vue +273 -264
  25. package/dist/runtime/app/components/Table/TableCell.vue +24 -73
  26. package/dist/runtime/app/theme/index.js +15 -1
  27. package/dist/runtime/app/types/table.d.ts +80 -13
  28. package/dist/runtime/app/types/theme.d.ts +6 -0
  29. package/dist/runtime/app/utils/column.d.ts +9 -0
  30. package/dist/runtime/app/utils/column.js +5 -0
  31. package/package.json +1 -1
@@ -5,6 +5,7 @@ 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
7
  import { getPath } from "#vuno/utils/path";
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 },
@@ -25,6 +26,8 @@ const messages = useVunoMessages();
25
26
  const ui = useVunoTheme("table", () => props.ui);
26
27
  const rowKey = computed(() => props.definition.rowKey ?? "id");
27
28
  const pageSizes = computed(() => props.definition.pageSizes ?? [10, 25, 50, 100]);
29
+ const searchable = computed(() => props.definition.searchable ?? true);
30
+ const paginated = computed(() => props.definition.paginated ?? true);
28
31
  const getRowId = (row) => String(getPath(row, rowKey.value) ?? "");
29
32
  const search = ref(query.value.search);
30
33
  let searchTimer;
@@ -152,197 +155,198 @@ function onRowSelect(_event, row) {
152
155
  </script>
153
156
 
154
157
  <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)))"
158
+ <div :class="ui.root">
159
+ <div :class="ui.toolbar">
160
+ <UInput
161
+ v-if="searchable"
162
+ ref="searchInput"
163
+ v-model="search"
164
+ :icon="icons.search"
165
+ :placeholder="props.definition.searchPlaceholder ?? messages.searchPlaceholder"
166
+ :ui="{ root: ui.search }"
167
+ >
168
+ <template
169
+ v-if="search.length > 0"
170
+ #trailing
171
+ >
172
+ <UButton
173
+ :aria-label="messages.clearSearch"
174
+ color="neutral"
175
+ :icon="icons.clearSearch"
176
+ size="xs"
177
+ variant="link"
178
+ @click="clearSearch"
179
+ />
180
+ </template>
181
+ </UInput>
182
+
183
+ <div :class="ui.toolbarActions">
184
+ <UButton
185
+ v-if="(props.definition.filters?.length ?? 0) > 0"
186
+ color="neutral"
187
+ :icon="icons.filter"
188
+ :label="messages.filters"
189
+ variant="outline"
190
+ @click="filtersOpen = true"
191
+ >
192
+ <!-- `UButton` no admite `badge`: el contador va en el slot. -->
193
+ <template
194
+ v-if="activeFilters.length > 0"
195
+ #trailing
196
+ >
197
+ <UBadge
198
+ color="primary"
199
+ :label="String(activeFilters.length)"
200
+ size="sm"
201
+ variant="solid"
202
+ />
203
+ </template>
204
+ </UButton>
205
+
206
+ <slot
207
+ name="toolbar"
208
+ :query="query"
209
+ />
210
+ </div>
211
+ </div>
212
+
213
+ <div
214
+ v-if="props.definition.selectable && selection.length > 0"
215
+ :class="ui.selection"
216
+ >
217
+ <slot
218
+ :clear="clearSelection"
219
+ :count="selection.length"
220
+ :keys="selection"
221
+ name="selection"
222
+ >
223
+ <span>{{ selection.length }} {{ selection.length === 1 ? messages.rowSelected : messages.rowsSelected }}</span>
224
+ <UButton
225
+ color="neutral"
226
+ :label="messages.clearSelection"
227
+ size="xs"
228
+ variant="ghost"
229
+ @click="clearSelection"
230
+ />
231
+ </slot>
232
+ </div>
233
+
234
+ <div
235
+ v-if="activeFilters.length > 0"
236
+ :class="ui.chips"
237
+ >
238
+ <UBadge
239
+ v-for="filter in activeFilters"
240
+ :key="filter.name"
241
+ color="neutral"
242
+ variant="subtle"
243
+ >
244
+ <span>{{ filter.label }}: {{ filter.value }}</span>
245
+ <UButton
246
+ :aria-label="`${messages.clearFilters}: ${filter.label}`"
247
+ color="neutral"
248
+ :icon="icons.fileRemove"
249
+ size="xs"
250
+ variant="link"
251
+ @click="removeFilter(filter.name)"
252
+ />
253
+ </UBadge>
254
+
255
+ <UButton
256
+ color="neutral"
257
+ :label="messages.clearFilters"
258
+ size="xs"
259
+ variant="ghost"
260
+ @click="clearFilters"
261
+ />
262
+ </div>
263
+
264
+ <USlideover
265
+ v-model:open="filtersOpen"
266
+ :title="messages.filtersTitle"
267
+ >
268
+ <template #body>
269
+ <Form
270
+ v-model:state="filterState"
271
+ :definition="filtersDefinition"
272
+ :ui="{ sections: ui.filtersBody }"
273
+ />
274
+ </template>
275
+
276
+ <template #footer>
277
+ <div :class="ui.filtersFooter">
278
+ <UButton
279
+ color="neutral"
280
+ :label="messages.clearFilters"
281
+ variant="ghost"
282
+ @click="clearFilters"
283
+ />
284
+ <UButton
285
+ :label="messages.applyFilters"
286
+ @click="applyFilters"
287
+ />
288
+ </div>
289
+ </template>
290
+ </USlideover>
291
+
292
+ <UTable
293
+ v-model:rowSelection="rowSelection"
294
+ :columns="columns"
295
+ :data="hasFailure ? [] : props.data"
296
+ :getRowId="getRowId"
297
+ :loading="props.loading ?? false"
298
+ :paginationOptions="{ manualPagination: true }"
299
+ :sortingOptions="{ manualSorting: true }"
300
+ :ui="{ base: ui.table, tbody: ui.tbody, td: ui.td, th: ui.th }"
301
+ @select="onRowSelect"
302
+ >
303
+ <template #select-header>
304
+ <UCheckbox
305
+ :modelValue="props.data.length > 0 && props.data.every((row2) => selection.includes(getRowId(row2)))"
302
306
  @update:model-value="
303
307
  (checked) => {
304
308
  const keys = props.data.map((row2) => getRowId(row2));
305
309
  selection = checked ? [.../* @__PURE__ */ new Set([...selection, ...keys])] : selection.filter((key) => !keys.includes(String(key)));
306
310
  }
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'"
311
+ "
312
+ />
313
+ </template>
314
+
315
+ <template #select-cell="{ row }">
316
+ <UCheckbox
317
+ :modelValue="row.getIsSelected()"
318
+ @update:model-value="row.toggleSelected(!row.getIsSelected())"
319
+ />
320
+ </template>
321
+
322
+ <template
323
+ v-for="column in props.definition.columns"
324
+ :key="`${column.key}-header`"
325
+ #[`${column.key}-header`]
326
+ >
327
+ <UButton
328
+ v-if="column.sortable"
329
+ class="-mx-2"
330
+ color="neutral"
331
+ :icon="sortIcon(column.key)"
332
+ :label="column.label ?? column.key"
333
+ size="xs"
334
+ trailing
335
+ variant="ghost"
336
+ @click="toggleSort(column.key)"
337
+ />
338
+ <span v-else>{{ column.type === "actions" ? column.label ?? "" : column.label ?? column.key }}</span>
339
+ </template>
340
+
341
+ <template
342
+ v-for="column in props.definition.columns"
343
+ :key="`${column.key}-cell`"
344
+ #[`${column.key}-cell`]="{ row }"
345
+ >
346
+ <!-- `stop`: sin esto, abrir el menú dispara también el clic de
347
+ fila y se acaban abriendo dos diálogos a la vez. -->
348
+ <UDropdownMenu
349
+ v-if="column.type === 'actions'"
346
350
  :items="[
347
351
  column.actions.filter((action) => !action.hidden?.(row.original)).map((action) => ({
348
352
  color: action.color,
@@ -351,82 +355,87 @@ function onRowSelect(_event, row) {
351
355
  label: action.label,
352
356
  onSelect: () => action.onSelect(row.original)
353
357
  }))
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>
358
+ ]"
359
+ @click.stop
360
+ >
361
+ <UButton
362
+ :aria-label="messages.rowActions"
363
+ color="neutral"
364
+ :icon="icons.actions"
365
+ size="xs"
366
+ variant="ghost"
367
+ />
368
+ </UDropdownMenu>
369
+
370
+ <slot
371
+ v-else
372
+ :name="`cell-${column.key}`"
373
+ :row="row.original"
374
+ :value="resolveColumnValue(column, row.original)"
375
+ >
376
+ <TableCell
377
+ :column="column"
378
+ :row="row.original"
379
+ />
380
+ </slot>
381
+ </template>
382
+
383
+ <template #empty>
384
+ <!-- Un fallo y una lista vacía no son lo mismo: el primero
385
+ ofrece reintentar, el segundo invita a crear el primero. -->
386
+ <FailureState
387
+ v-if="props.failure"
388
+ :failure="props.failure"
389
+ />
390
+
391
+ <!-- Cargando y sin filas todavía no es «no hay nada»: decirlo
392
+ antes de tiempo hace parpadear la tabla en cada cambio. -->
393
+ <div v-else-if="props.loading" />
394
+
395
+ <slot
396
+ v-else
397
+ name="empty"
398
+ >
399
+ <EmptyState
400
+ :hint="props.definition.empty?.hint"
401
+ :icon="props.definition.empty?.icon"
402
+ :title="props.definition.empty?.title"
403
+ >
404
+ <template #actions>
405
+ <slot name="empty-actions" />
406
+ </template>
407
+ </EmptyState>
408
+ </slot>
409
+ </template>
410
+ </UTable>
411
+
412
+ <div
413
+ v-if="total > 0 && !hasFailure"
414
+ :class="ui.footer"
415
+ >
416
+ <div :class="ui.meta">
417
+ <span v-if="paginated">{{ messages.showing }} {{ rangeStart }}–{{ rangeEnd }} {{ messages.of }} {{ total }}</span>
418
+
419
+ <!-- Sin paginar, un rango «1–30 de 30» sugiere que hay más. -->
420
+ <span v-else>{{ total }} {{ messages.results }}</span>
421
+
422
+ <USelect
423
+ v-if="paginated"
424
+ :items="pageSizes"
425
+ :modelValue="query.pageSize"
426
+ size="xs"
427
+ :ui="{ base: 'w-20' }"
428
+ @update:model-value="(size) => onPageSizeChange(Number(size))"
429
+ />
430
+ </div>
431
+
432
+ <UPagination
433
+ v-if="paginated"
434
+ :itemsPerPage="query.pageSize"
435
+ :page="query.page"
436
+ :total="total"
437
+ @update:page="onPageChange"
438
+ />
439
+ </div>
440
+ </div>
432
441
  </template>