rl-core-front 0.15.4 → 0.15.6
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/package.json +1 -1
- package/src/_utils/calendar.ts +211 -7
- package/src/components/ui/data-table.tsx +632 -247
- package/src/components/ui/date-picker.tsx +84 -10
- package/src/components/ui/dialog.tsx +81 -1
- package/src/components/ui/table.tsx +30 -1
- package/src/hooks/use-column-visibility.ts +6 -1
- package/src/hooks/use-table-view.ts +54 -0
- package/src/i18n/messages/en.ts +5 -0
- package/src/i18n/messages/pt.ts +5 -0
- package/src/index.ts +2 -0
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
SlidersHorizontal,
|
|
31
31
|
} from "lucide-react";
|
|
32
32
|
import type { JSX, PointerEvent as ReactPointerEvent } from "react";
|
|
33
|
-
import { useMemo, useRef, useState } from "react";
|
|
33
|
+
import { Fragment, useMemo, useRef, useState } from "react";
|
|
34
34
|
|
|
35
35
|
import { countConditions } from "#core/_utils/advanced-filter";
|
|
36
36
|
import { activeFilterCount } from "#core/_utils/filter";
|
|
@@ -42,11 +42,13 @@ import {
|
|
|
42
42
|
ColumnVisibilityOption,
|
|
43
43
|
} from "#core/components/ui/column-visibility-modal";
|
|
44
44
|
import { FilterSheet } from "#core/components/ui/filter-sheet";
|
|
45
|
+
import { SegmentedControl } from "#core/components/ui/segmented-control";
|
|
45
46
|
import { Spinner } from "#core/components/ui/spinner";
|
|
46
47
|
import {
|
|
47
48
|
Table,
|
|
48
49
|
TableBody,
|
|
49
50
|
TableCell,
|
|
51
|
+
TableFooter,
|
|
50
52
|
TableHead,
|
|
51
53
|
TableHeader,
|
|
52
54
|
TableRow,
|
|
@@ -54,6 +56,8 @@ import {
|
|
|
54
56
|
import { useI18n } from "#core/contexts";
|
|
55
57
|
import { useColumnVisibility } from "#core/hooks/use-column-visibility";
|
|
56
58
|
import type { FiltersState } from "#core/hooks/use-filters";
|
|
59
|
+
import type { TableView } from "#core/hooks/use-table-view";
|
|
60
|
+
import { useTableView } from "#core/hooks/use-table-view";
|
|
57
61
|
import { cn } from "#core/lib/utils";
|
|
58
62
|
|
|
59
63
|
export const dataTableFeatures = tableFeatures({
|
|
@@ -79,6 +83,14 @@ interface DataTableProps<T extends RowData> {
|
|
|
79
83
|
loading?: boolean;
|
|
80
84
|
/** Ações à esquerda da toolbar (ex.: botão "Novo"). */
|
|
81
85
|
toolbar?: React.ReactNode;
|
|
86
|
+
/**
|
|
87
|
+
* Ações à direita, antes de "Filtros" e "Colunas".
|
|
88
|
+
*
|
|
89
|
+
* É onde mora o que age sobre **a listagem inteira** — exportar, por
|
|
90
|
+
* exemplo. À esquerda ficam as ações de criar; misturar as duas coisas na
|
|
91
|
+
* mesma ponta faz o botão de exportar parecer mais um "novo".
|
|
92
|
+
*/
|
|
93
|
+
toolbarEnd?: React.ReactNode;
|
|
82
94
|
pageSizeOptions?: number[];
|
|
83
95
|
/** Paginação no servidor: informe rowCount, pagination e onPaginationChange. */
|
|
84
96
|
manualPagination?: boolean;
|
|
@@ -115,6 +127,18 @@ interface DataTableProps<T extends RowData> {
|
|
|
115
127
|
* indistinguível de bug, e manda o usuário procurar defeito onde não há.
|
|
116
128
|
*/
|
|
117
129
|
emptyMessage?: string;
|
|
130
|
+
/**
|
|
131
|
+
* Esconde o rodapé de contagem, paginação e "por página".
|
|
132
|
+
*
|
|
133
|
+
* Para a listagem que já vem inteira numa resposta — um cadastro de vinte
|
|
134
|
+
* linhas não precisa de página. Sem o rodapé a tabela mostra **todas** as
|
|
135
|
+
* linhas, senão esconder a navegação esconderia registros.
|
|
136
|
+
*
|
|
137
|
+
* Ignorado quando a paginação é do servidor (`manualPagination`): ali a
|
|
138
|
+
* tabela só tem em mãos a página atual, e tirar o rodapé faria as outras
|
|
139
|
+
* sumirem sem aviso.
|
|
140
|
+
*/
|
|
141
|
+
hideFooter?: boolean;
|
|
118
142
|
/**
|
|
119
143
|
* Arrastar a linha pela alça para reordenar a listagem.
|
|
120
144
|
*
|
|
@@ -128,6 +152,66 @@ interface DataTableProps<T extends RowData> {
|
|
|
128
152
|
* gravada, e arrastar escreveria uma sequência que ninguém viu.
|
|
129
153
|
*/
|
|
130
154
|
onRowReorder?: (activeId: string, overId: string) => void;
|
|
155
|
+
/**
|
|
156
|
+
* Desenha a listagem em cartões, e não em linhas.
|
|
157
|
+
*
|
|
158
|
+
* Recebe as linhas da página atual, já ordenadas e filtradas, e devolve o
|
|
159
|
+
* que aparece no lugar da tabela. Com esta prop a barra ganha o alternador
|
|
160
|
+
* lista/cartões, e a escolha fica guardada por `storageKey` — dado que se vê
|
|
161
|
+
* (uma coleção, um catálogo de fotos) se compara olhando, e dado que se
|
|
162
|
+
* confere se compara lendo; a mesma listagem serve às duas coisas.
|
|
163
|
+
*
|
|
164
|
+
* Em cartões não há cabeçalho, logo não há por onde ordenar: a ordem
|
|
165
|
+
* escolhida na lista continua valendo, e o botão de colunas some — cartão
|
|
166
|
+
* não tem coluna para esconder.
|
|
167
|
+
*/
|
|
168
|
+
renderGrid?: (rows: T[]) => React.ReactNode;
|
|
169
|
+
/** Como a listagem abre na primeira visita, antes de alguém escolher. */
|
|
170
|
+
defaultView?: TableView;
|
|
171
|
+
/**
|
|
172
|
+
* A que seção cada linha pertence — sem isto, não há agrupamento.
|
|
173
|
+
*
|
|
174
|
+
* O grupo é sempre a chave primária da ordem: ordenar por uma coluna ordena
|
|
175
|
+
* **dentro** da seção. Do contrário, um clique no cabeçalho intercalaria as
|
|
176
|
+
* seções e o cabeçalho de grupo reapareceria a cada troca de valor — dez
|
|
177
|
+
* faixas onde existem duas.
|
|
178
|
+
*/
|
|
179
|
+
groupBy?: (row: T) => string;
|
|
180
|
+
/**
|
|
181
|
+
* O cabeçalho de cada seção: uma linha que atravessa a tabela inteira.
|
|
182
|
+
*
|
|
183
|
+
* Recebe a chave e as linhas da seção — é de onde saem contagem e total sem
|
|
184
|
+
* o consumidor precisar refazer a divisão que a tabela acabou de fazer. Sem
|
|
185
|
+
* esta prop o agrupamento só ordena, e nada é desenhado.
|
|
186
|
+
*/
|
|
187
|
+
renderGroupHeader?: (group: DataTableGroup<T>) => React.ReactNode;
|
|
188
|
+
/**
|
|
189
|
+
* A faixa de cada seção fecha e abre no clique.
|
|
190
|
+
*
|
|
191
|
+
* Fechada, a seção fica só na faixa — e é por isso que a faixa recebe
|
|
192
|
+
* `collapsed`: quem a desenha decide o que dizer quando as linhas não estão
|
|
193
|
+
* à vista (a contagem, em geral, que aberta é redundante). A escolha vale
|
|
194
|
+
* enquanto a listagem estiver na tela; não é gravada.
|
|
195
|
+
*/
|
|
196
|
+
collapsibleGroups?: boolean;
|
|
197
|
+
/**
|
|
198
|
+
* A ordem das seções.
|
|
199
|
+
*
|
|
200
|
+
* Chave que não aparecer aqui vai para o fim, na ordem em que apareceu nos
|
|
201
|
+
* dados. Sem a prop, a ordem é a de chegada — e "a de chegada" muda quando
|
|
202
|
+
* alguém ordena por uma coluna, por isso toda listagem com seções fixas
|
|
203
|
+
* (a receber / recebidas) declara esta lista.
|
|
204
|
+
*/
|
|
205
|
+
groupOrder?: string[];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Uma seção da listagem, como o cabeçalho de grupo a recebe. */
|
|
209
|
+
export interface DataTableGroup<T> {
|
|
210
|
+
key: string;
|
|
211
|
+
/** As linhas da seção — todas, mesmo com ela fechada. */
|
|
212
|
+
rows: T[];
|
|
213
|
+
/** Sempre `false` sem `collapsibleGroups`. */
|
|
214
|
+
collapsed: boolean;
|
|
131
215
|
}
|
|
132
216
|
|
|
133
217
|
/** Extras que uma coluna pode declarar em `meta`. */
|
|
@@ -139,6 +223,33 @@ export interface DataTableColumnMeta {
|
|
|
139
223
|
* É o caso da coluna de ações: sem isso o `<table>` a esticava.
|
|
140
224
|
*/
|
|
141
225
|
shrink?: boolean;
|
|
226
|
+
/**
|
|
227
|
+
* A coluna existe, mas não aparece até alguém ligá-la em "Colunas".
|
|
228
|
+
*
|
|
229
|
+
* Para o dado que a listagem tem e quase ninguém procura — a loja, o dia da
|
|
230
|
+
* compra — em tabela que já mostra o essencial. Vale só na primeira visita:
|
|
231
|
+
* a partir do momento em que alguém escolhe as colunas, a escolha guardada é
|
|
232
|
+
* que manda, e uma coluna nova nascendo escondida não mexe no que a pessoa
|
|
233
|
+
* já arrumou.
|
|
234
|
+
*/
|
|
235
|
+
defaultHidden?: boolean;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* O id que a coluna terá na tabela: o declarado, ou a chave do acessor.
|
|
240
|
+
*
|
|
241
|
+
* É o mesmo par que o TanStack usa para nomear a coluna, e precisa ser lido
|
|
242
|
+
* antes dela existir — o padrão de visibilidade entra na montagem da tabela.
|
|
243
|
+
*/
|
|
244
|
+
function columnId<T extends RowData>(
|
|
245
|
+
col: ColumnDef<DataTableFeatures, T, unknown>,
|
|
246
|
+
): string | undefined {
|
|
247
|
+
if (col.id) {
|
|
248
|
+
return col.id;
|
|
249
|
+
}
|
|
250
|
+
const acessor = (col as { accessorKey?: unknown }).accessorKey;
|
|
251
|
+
|
|
252
|
+
return typeof acessor === "string" ? acessor : undefined;
|
|
142
253
|
}
|
|
143
254
|
|
|
144
255
|
function columnLabel<T extends RowData>(
|
|
@@ -155,6 +266,85 @@ function columnLabel<T extends RowData>(
|
|
|
155
266
|
return id;
|
|
156
267
|
}
|
|
157
268
|
|
|
269
|
+
/**
|
|
270
|
+
* As linhas repartidas em seções, na ordem em que as seções saem.
|
|
271
|
+
*
|
|
272
|
+
* Estável de propósito: dentro da seção, as linhas mantêm a ordem que a tabela
|
|
273
|
+
* já lhes deu — a ordenação por coluna continua valendo, só que dentro de cada
|
|
274
|
+
* faixa. Seção declarada em `order` que não tiver nenhuma linha não vira faixa
|
|
275
|
+
* vazia: some, como o bloco vazio de uma listagem some.
|
|
276
|
+
*/
|
|
277
|
+
function groupRows<T, R extends { original: T }>(
|
|
278
|
+
rows: R[],
|
|
279
|
+
groupBy: (row: T) => string,
|
|
280
|
+
order?: string[],
|
|
281
|
+
): { key: string; rows: R[] }[] {
|
|
282
|
+
const buckets = new Map<string, R[]>();
|
|
283
|
+
|
|
284
|
+
rows.forEach((row) => {
|
|
285
|
+
const key = groupBy(row.original);
|
|
286
|
+
const bucket = buckets.get(key);
|
|
287
|
+
if (bucket) {
|
|
288
|
+
bucket.push(row);
|
|
289
|
+
} else {
|
|
290
|
+
buckets.set(key, [row]);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const declared = (order ?? []).filter((key) => buckets.has(key));
|
|
295
|
+
const rest = [...buckets.keys()].filter((key) => !declared.includes(key));
|
|
296
|
+
|
|
297
|
+
return [...declared, ...rest].map((key) => ({
|
|
298
|
+
key,
|
|
299
|
+
rows: buckets.get(key) ?? [],
|
|
300
|
+
}));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* O corpo da tabela em seções, com o índice de onde cada uma começa.
|
|
305
|
+
*
|
|
306
|
+
* Uma seção só, sem faixa, quando não há agrupamento — assim o corpo se
|
|
307
|
+
* desenha do mesmo jeito nos dois casos, em vez de dois caminhos que precisam
|
|
308
|
+
* ser mantidos iguais. O `offset` é a posição da primeira linha da seção na
|
|
309
|
+
* lista visível: é dele que sai o índice que o arrasto usa para medir o
|
|
310
|
+
* deslocamento.
|
|
311
|
+
*/
|
|
312
|
+
function bodySections<T, R extends { original: T }>(
|
|
313
|
+
rows: R[],
|
|
314
|
+
groupBy: ((row: T) => string) | undefined,
|
|
315
|
+
order: string[] | undefined,
|
|
316
|
+
collapsedKeys: string[],
|
|
317
|
+
): {
|
|
318
|
+
key: string;
|
|
319
|
+
offset: number;
|
|
320
|
+
rows: R[];
|
|
321
|
+
header: DataTableGroup<T> | null;
|
|
322
|
+
}[] {
|
|
323
|
+
if (!groupBy) {
|
|
324
|
+
return [{ key: "", offset: 0, rows, header: null }];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
let offset = 0;
|
|
328
|
+
|
|
329
|
+
return groupRows(rows, groupBy, order).map((group) => {
|
|
330
|
+
const collapsed = collapsedKeys.includes(group.key);
|
|
331
|
+
const section = {
|
|
332
|
+
key: group.key,
|
|
333
|
+
offset,
|
|
334
|
+
rows: collapsed ? [] : group.rows,
|
|
335
|
+
header: {
|
|
336
|
+
key: group.key,
|
|
337
|
+
rows: group.rows.map((row) => row.original),
|
|
338
|
+
collapsed,
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
offset += section.rows.length;
|
|
343
|
+
|
|
344
|
+
return section;
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
158
348
|
/**
|
|
159
349
|
* `w-px` + `whitespace-nowrap` é o jeito de fazer uma coluna de tabela encolher
|
|
160
350
|
* até o conteúdo: o navegador trata a largura mínima como o conteúdo e o espaço
|
|
@@ -172,6 +362,7 @@ export function DataTable<T extends RowData>({
|
|
|
172
362
|
storageKey,
|
|
173
363
|
loading,
|
|
174
364
|
toolbar,
|
|
365
|
+
toolbarEnd,
|
|
175
366
|
pageSizeOptions = [10, 20, 50],
|
|
176
367
|
manualPagination,
|
|
177
368
|
rowCount,
|
|
@@ -184,10 +375,19 @@ export function DataTable<T extends RowData>({
|
|
|
184
375
|
filters,
|
|
185
376
|
hiddenFilterFields,
|
|
186
377
|
emptyMessage,
|
|
378
|
+
hideFooter = false,
|
|
187
379
|
onRowReorder,
|
|
380
|
+
renderGrid,
|
|
381
|
+
defaultView = "list",
|
|
382
|
+
groupBy,
|
|
383
|
+
renderGroupHeader,
|
|
384
|
+
groupOrder,
|
|
385
|
+
collapsibleGroups = false,
|
|
188
386
|
}: DataTableProps<T>): JSX.Element {
|
|
189
387
|
const { t } = useI18n();
|
|
190
388
|
const [colsOpen, setColsOpen] = useState(false);
|
|
389
|
+
/** As seções fechadas, por chave. */
|
|
390
|
+
const [collapsedGroups, setCollapsedGroups] = useState<string[]>([]);
|
|
191
391
|
const [filtersOpen, setFiltersOpen] = useState(false);
|
|
192
392
|
/** A linha na mão e a linha embaixo do dedo, enquanto o arrasto dura. */
|
|
193
393
|
const [draggedRow, setDraggedRow] = useState<string | null>(null);
|
|
@@ -211,7 +411,22 @@ export function DataTable<T extends RowData>({
|
|
|
211
411
|
*/
|
|
212
412
|
const [dragHeight, setDragHeight] = useState(0);
|
|
213
413
|
|
|
214
|
-
const {
|
|
414
|
+
const { view, setView } = useTableView(storageKey, defaultView);
|
|
415
|
+
|
|
416
|
+
const ocultasPorPadrao = useMemo(
|
|
417
|
+
() =>
|
|
418
|
+
columns
|
|
419
|
+
.filter((column) => (column.meta as DataTableColumnMeta)?.defaultHidden)
|
|
420
|
+
.map((column) => columnId(column))
|
|
421
|
+
.filter((id): id is string => Boolean(id)),
|
|
422
|
+
[columns],
|
|
423
|
+
);
|
|
424
|
+
const emCartoes = Boolean(renderGrid) && view === "grid";
|
|
425
|
+
|
|
426
|
+
const { hiddenColumns, saveHiddenColumns } = useColumnVisibility(
|
|
427
|
+
storageKey,
|
|
428
|
+
ocultasPorPadrao,
|
|
429
|
+
);
|
|
215
430
|
const columnVisibility = useMemo<ColumnVisibilityState>(() => {
|
|
216
431
|
const v: ColumnVisibilityState = {};
|
|
217
432
|
hiddenColumns.forEach((id) => {
|
|
@@ -228,6 +443,16 @@ export function DataTable<T extends RowData>({
|
|
|
228
443
|
},
|
|
229
444
|
);
|
|
230
445
|
|
|
446
|
+
/*
|
|
447
|
+
Rodapé escondido é lista inteira na tela: sem a navegação, um `pageSize` de
|
|
448
|
+
10 esconderia da décima primeira linha em diante sem nada dizer. Não vale
|
|
449
|
+
para a paginação do servidor, onde a tabela só tem a página atual em mãos.
|
|
450
|
+
*/
|
|
451
|
+
const semRodape = hideFooter && !manualPagination;
|
|
452
|
+
const paginacaoLocal = semRodape
|
|
453
|
+
? { pageIndex: 0, pageSize: Math.max(data.length, 1) }
|
|
454
|
+
: internalPagination;
|
|
455
|
+
|
|
231
456
|
const table = useTable({
|
|
232
457
|
features: dataTableFeatures,
|
|
233
458
|
data,
|
|
@@ -236,7 +461,7 @@ export function DataTable<T extends RowData>({
|
|
|
236
461
|
state: {
|
|
237
462
|
columnVisibility,
|
|
238
463
|
sorting: manualSorting ? sorting : internalSorting,
|
|
239
|
-
pagination: manualPagination ? pagination :
|
|
464
|
+
pagination: manualPagination ? pagination : paginacaoLocal,
|
|
240
465
|
},
|
|
241
466
|
onColumnVisibilityChange: (updater) => {
|
|
242
467
|
const next =
|
|
@@ -257,6 +482,17 @@ export function DataTable<T extends RowData>({
|
|
|
257
482
|
...(manualPagination ? { rowCount: rowCount ?? 0 } : {}),
|
|
258
483
|
});
|
|
259
484
|
|
|
485
|
+
/*
|
|
486
|
+
Linha de fechamento: existe só se alguma coluna visível declarar `footer`.
|
|
487
|
+
|
|
488
|
+
Vem do `columnDef` e não de uma última linha no `data` porque totalizador
|
|
489
|
+
não é registro — no corpo, ele ordenaria junto com os outros e apareceria no
|
|
490
|
+
meio da tabela à primeira troca de coluna.
|
|
491
|
+
*/
|
|
492
|
+
const temFechamento = table
|
|
493
|
+
.getAllColumns()
|
|
494
|
+
.some((c) => c.getIsVisible() && Boolean(c.columnDef.footer));
|
|
495
|
+
|
|
260
496
|
const hideable: ColumnVisibilityOption[] = table
|
|
261
497
|
.getAllColumns()
|
|
262
498
|
.filter((c) => c.getCanHide())
|
|
@@ -280,7 +516,30 @@ export function DataTable<T extends RowData>({
|
|
|
280
516
|
A coluna da alça continua no lugar para a tabela não mudar de largura — só
|
|
281
517
|
o botão fica inerte, e o `title` diz o motivo.
|
|
282
518
|
*/
|
|
283
|
-
const canReorder =
|
|
519
|
+
const canReorder =
|
|
520
|
+
reorderable && (table.state.sorting ?? []).length === 0 && !groupBy;
|
|
521
|
+
|
|
522
|
+
/*
|
|
523
|
+
A lista já repartida em seções, achatada de volta numa sequência.
|
|
524
|
+
|
|
525
|
+
Achatada, e não desenhada como uma tabela por seção: seção que vira tabela
|
|
526
|
+
própria mede as próprias linhas, e a mesma coluna acaba com uma largura em
|
|
527
|
+
cada faixa. Uma tabela só é o que garante que a coluna vale o mesmo de cima
|
|
528
|
+
a baixo — sem largura fixa em lugar nenhum.
|
|
529
|
+
*/
|
|
530
|
+
const sections = bodySections(
|
|
531
|
+
table.getRowModel().rows,
|
|
532
|
+
groupBy,
|
|
533
|
+
groupOrder,
|
|
534
|
+
collapsedGroups,
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
const toggleGroup = (key: string): void =>
|
|
538
|
+
setCollapsedGroups((atual) =>
|
|
539
|
+
atual.includes(key)
|
|
540
|
+
? atual.filter((fechada) => fechada !== key)
|
|
541
|
+
: [...atual, key],
|
|
542
|
+
);
|
|
284
543
|
const colSpan =
|
|
285
544
|
(table.getVisibleLeafColumns().length || 1) + (reorderable ? 1 : 0);
|
|
286
545
|
|
|
@@ -373,6 +632,18 @@ export function DataTable<T extends RowData>({
|
|
|
373
632
|
};
|
|
374
633
|
|
|
375
634
|
const filtersEnabled = Boolean(filters);
|
|
635
|
+
const columnsButton =
|
|
636
|
+
Boolean(storageKey) && hideable.length > 0 && !emCartoes;
|
|
637
|
+
/*
|
|
638
|
+
A barra só existe se tiver o que mostrar.
|
|
639
|
+
|
|
640
|
+
Vazia, ela continuava ocupando os 12px do `mb-3` e empurrava a tabela para
|
|
641
|
+
baixo — a listagem sem toolbar, sem filtro e sem escolha de colunas ficava
|
|
642
|
+
com um vão maior que o das outras, sem nada que o explicasse.
|
|
643
|
+
*/
|
|
644
|
+
const temBarra = Boolean(
|
|
645
|
+
toolbar || toolbarEnd || renderGrid || filtersEnabled || columnsButton,
|
|
646
|
+
);
|
|
376
647
|
const activeFilters = !filters
|
|
377
648
|
? 0
|
|
378
649
|
: filters.mode === "advanced"
|
|
@@ -388,6 +659,82 @@ export function DataTable<T extends RowData>({
|
|
|
388
659
|
emptyMessage ??
|
|
389
660
|
(activeFilters > 0 ? t("table.emptyFiltered") : t("table.empty"));
|
|
390
661
|
|
|
662
|
+
/*
|
|
663
|
+
Três blocos, e a ordem tem motivo: quantos são à esquerda, para onde ir no
|
|
664
|
+
meio, e o tamanho da página à direita — o controle que se mexe uma vez fica
|
|
665
|
+
na ponta, e a navegação, que se usa o tempo todo, no centro. Em tela estreita
|
|
666
|
+
eles empilham em vez de espremer.
|
|
667
|
+
|
|
668
|
+
Fora do JSX da tabela porque a mesma navegação serve à lista e aos cartões:
|
|
669
|
+
o formato muda o que está acima dele, não em que página se está.
|
|
670
|
+
*/
|
|
671
|
+
const rodape = (
|
|
672
|
+
<div className="flex flex-col items-center gap-3 border-t border-border px-4 py-3 text-sm text-muted-foreground sm:flex-row sm:justify-between">
|
|
673
|
+
<span className="sm:flex-1">
|
|
674
|
+
{total} {t("table.results")}
|
|
675
|
+
</span>
|
|
676
|
+
|
|
677
|
+
<div className="flex items-center gap-1">
|
|
678
|
+
<Button
|
|
679
|
+
variant="ghost"
|
|
680
|
+
size="icon"
|
|
681
|
+
aria-label={t("table.previousPage")}
|
|
682
|
+
disabled={!table.getCanPreviousPage()}
|
|
683
|
+
onClick={() => table.previousPage()}
|
|
684
|
+
>
|
|
685
|
+
<ChevronLeft className="h-4 w-4" />
|
|
686
|
+
</Button>
|
|
687
|
+
|
|
688
|
+
{pageRange(pageIndex, pageCount).map((item, posicao) =>
|
|
689
|
+
item === "gap" ? (
|
|
690
|
+
// O buraco não é botão: `key` pela posição porque duas elipses
|
|
691
|
+
// são indistinguíveis entre si.
|
|
692
|
+
<span key={`gap-${posicao}`} className="px-1 text-muted-foreground">
|
|
693
|
+
…
|
|
694
|
+
</span>
|
|
695
|
+
) : (
|
|
696
|
+
<Button
|
|
697
|
+
key={item}
|
|
698
|
+
variant={item === pageIndex + 1 ? "default" : "ghost"}
|
|
699
|
+
size="icon"
|
|
700
|
+
// Quadrado de 40px como base, mas cresce com o número: a
|
|
701
|
+
// página 128 não pode vazar do botão.
|
|
702
|
+
className="w-auto min-w-10 px-2 tabular-nums"
|
|
703
|
+
aria-current={item === pageIndex + 1 ? "page" : undefined}
|
|
704
|
+
onClick={() => table.setPageIndex(item - 1)}
|
|
705
|
+
>
|
|
706
|
+
{item}
|
|
707
|
+
</Button>
|
|
708
|
+
),
|
|
709
|
+
)}
|
|
710
|
+
|
|
711
|
+
<Button
|
|
712
|
+
variant="ghost"
|
|
713
|
+
size="icon"
|
|
714
|
+
aria-label={t("table.nextPage")}
|
|
715
|
+
disabled={!table.getCanNextPage()}
|
|
716
|
+
onClick={() => table.nextPage()}
|
|
717
|
+
>
|
|
718
|
+
<ChevronRight className="h-4 w-4" />
|
|
719
|
+
</Button>
|
|
720
|
+
</div>
|
|
721
|
+
|
|
722
|
+
<div className="sm:flex-1 sm:text-right">
|
|
723
|
+
<select
|
|
724
|
+
value={pageSize}
|
|
725
|
+
onChange={(e) => table.setPageSize(Number(e.target.value))}
|
|
726
|
+
className="h-9 rounded-md border border-input bg-transparent px-2 text-sm"
|
|
727
|
+
>
|
|
728
|
+
{pageSizeOptions.map((n) => (
|
|
729
|
+
<option key={n} value={n}>
|
|
730
|
+
{n} {t("table.perPage")}
|
|
731
|
+
</option>
|
|
732
|
+
))}
|
|
733
|
+
</select>
|
|
734
|
+
</div>
|
|
735
|
+
</div>
|
|
736
|
+
);
|
|
737
|
+
|
|
391
738
|
return (
|
|
392
739
|
<div>
|
|
393
740
|
{/*
|
|
@@ -399,269 +746,307 @@ export function DataTable<T extends RowData>({
|
|
|
399
746
|
`min-w-0` é o par obrigatório disso — filho de flex não encolhe abaixo
|
|
400
747
|
do próprio conteúdo sem ele, e o `flex-wrap` sozinho não bastaria.
|
|
401
748
|
*/}
|
|
402
|
-
|
|
403
|
-
<div className="
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
749
|
+
{temBarra && (
|
|
750
|
+
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
|
|
751
|
+
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
|
752
|
+
{toolbar}
|
|
753
|
+
</div>
|
|
754
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
755
|
+
{renderGrid && (
|
|
756
|
+
<SegmentedControl
|
|
757
|
+
label={t("table.view")}
|
|
758
|
+
value={view}
|
|
759
|
+
onValueChange={(next) => setView(next as TableView)}
|
|
760
|
+
options={[
|
|
761
|
+
{ value: "list", label: t("table.viewList") },
|
|
762
|
+
{ value: "grid", label: t("table.viewGrid") },
|
|
763
|
+
]}
|
|
764
|
+
/>
|
|
765
|
+
)}
|
|
766
|
+
{toolbarEnd}
|
|
767
|
+
{/* Filtro antes de colunas: filtrar é a ação do dia a dia, escolher
|
|
408
768
|
coluna se faz uma vez e a tela guarda. */}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
769
|
+
{filtersEnabled && (
|
|
770
|
+
<Button
|
|
771
|
+
variant="outline"
|
|
772
|
+
size="sm"
|
|
773
|
+
onClick={() => setFiltersOpen(true)}
|
|
774
|
+
>
|
|
775
|
+
<Filter className="h-4 w-4" />
|
|
776
|
+
{t("filters.button")}
|
|
777
|
+
{activeFilters > 0 && (
|
|
778
|
+
<span className="ml-1 rounded-full bg-primary px-1.5 text-xs text-primary-foreground">
|
|
779
|
+
{activeFilters}
|
|
780
|
+
</span>
|
|
781
|
+
)}
|
|
782
|
+
</Button>
|
|
783
|
+
)}
|
|
784
|
+
{columnsButton && (
|
|
785
|
+
<Button
|
|
786
|
+
variant="outline"
|
|
787
|
+
size="sm"
|
|
788
|
+
onClick={() => setColsOpen(true)}
|
|
789
|
+
>
|
|
790
|
+
<SlidersHorizontal className="h-4 w-4" />
|
|
791
|
+
{t("table.columnsButton")}
|
|
792
|
+
</Button>
|
|
793
|
+
)}
|
|
794
|
+
</div>
|
|
795
|
+
</div>
|
|
796
|
+
)}
|
|
797
|
+
|
|
798
|
+
{emCartoes && renderGrid ? (
|
|
799
|
+
<div className="flex flex-col gap-3">
|
|
800
|
+
{loading && (
|
|
801
|
+
<Card className="p-10">
|
|
802
|
+
<Spinner className="mx-auto" />
|
|
803
|
+
</Card>
|
|
423
804
|
)}
|
|
424
|
-
{
|
|
425
|
-
<
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
onClick={() => setColsOpen(true)}
|
|
429
|
-
>
|
|
430
|
-
<SlidersHorizontal className="h-4 w-4" />
|
|
431
|
-
{t("table.columnsButton")}
|
|
432
|
-
</Button>
|
|
805
|
+
{!loading && table.getRowModel().rows.length === 0 && (
|
|
806
|
+
<Card className="p-10 text-center text-muted-foreground">
|
|
807
|
+
{emptyText}
|
|
808
|
+
</Card>
|
|
433
809
|
)}
|
|
434
|
-
|
|
435
|
-
|
|
810
|
+
{!loading &&
|
|
811
|
+
table.getRowModel().rows.length > 0 &&
|
|
812
|
+
renderGrid(table.getRowModel().rows.map((row) => row.original))}
|
|
436
813
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
<
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
814
|
+
{/* O rodapé ganha moldura própria: solto sobre o fundo, a navegação
|
|
815
|
+
ficaria pendurada abaixo dos cartões, sem nada que a segure. */}
|
|
816
|
+
{!semRodape && <Card>{rodape}</Card>}
|
|
817
|
+
</div>
|
|
818
|
+
) : (
|
|
819
|
+
<Card>
|
|
820
|
+
<Table>
|
|
821
|
+
<TableHeader>
|
|
822
|
+
{table.getHeaderGroups().map((hg) => (
|
|
823
|
+
<TableRow key={hg.id}>
|
|
824
|
+
{reorderable && (
|
|
825
|
+
<TableHead className="w-px">
|
|
826
|
+
<span className="sr-only">{t("table.reorder")}</span>
|
|
827
|
+
</TableHead>
|
|
828
|
+
)}
|
|
829
|
+
{hg.headers.map((header) => {
|
|
830
|
+
const canSort = header.column.getCanSort();
|
|
831
|
+
const sorted = header.column.getIsSorted();
|
|
832
|
+
return (
|
|
833
|
+
<TableHead
|
|
834
|
+
key={header.id}
|
|
835
|
+
className={shrinkClass(header.column.columnDef)}
|
|
836
|
+
>
|
|
837
|
+
{header.isPlaceholder ? null : canSort ? (
|
|
838
|
+
<button
|
|
839
|
+
className="flex items-center gap-1 hover:text-foreground"
|
|
840
|
+
onClick={header.column.getToggleSortingHandler()}
|
|
841
|
+
>
|
|
842
|
+
{flexRender(
|
|
843
|
+
header.column.columnDef.header,
|
|
844
|
+
header.getContext(),
|
|
845
|
+
)}
|
|
846
|
+
{sorted === "asc" ? (
|
|
847
|
+
<ArrowUp className="h-3.5 w-3.5 text-primary" />
|
|
848
|
+
) : sorted === "desc" ? (
|
|
849
|
+
<ArrowDown className="h-3.5 w-3.5 text-primary" />
|
|
850
|
+
) : (
|
|
851
|
+
// Seta dupla apagada na coluna livre e seta única
|
|
852
|
+
// em destaque na que ordena: sem a cor, achar a
|
|
853
|
+
// coluna ativa exige comparar o desenho de todas.
|
|
854
|
+
<ArrowUpDown className="h-3.5 w-3.5 opacity-40" />
|
|
855
|
+
)}
|
|
856
|
+
</button>
|
|
857
|
+
) : (
|
|
858
|
+
flexRender(
|
|
461
859
|
header.column.columnDef.header,
|
|
462
860
|
header.getContext(),
|
|
463
|
-
)
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
861
|
+
)
|
|
862
|
+
)}
|
|
863
|
+
</TableHead>
|
|
864
|
+
);
|
|
865
|
+
})}
|
|
866
|
+
</TableRow>
|
|
867
|
+
))}
|
|
868
|
+
</TableHeader>
|
|
869
|
+
<TableBody>
|
|
870
|
+
{loading && (
|
|
871
|
+
<TableRow>
|
|
872
|
+
<TableCell colSpan={colSpan} className="h-24 text-center">
|
|
873
|
+
<Spinner className="mx-auto" />
|
|
874
|
+
</TableCell>
|
|
875
|
+
</TableRow>
|
|
876
|
+
)}
|
|
877
|
+
{!loading && table.getRowModel().rows.length === 0 && (
|
|
878
|
+
<TableRow>
|
|
879
|
+
<TableCell
|
|
880
|
+
colSpan={colSpan}
|
|
881
|
+
className="h-24 text-center text-muted-foreground"
|
|
882
|
+
>
|
|
883
|
+
{emptyText}
|
|
884
|
+
</TableCell>
|
|
885
|
+
</TableRow>
|
|
886
|
+
)}
|
|
887
|
+
{!loading &&
|
|
888
|
+
sections.map((section) => (
|
|
889
|
+
<Fragment key={section.key}>
|
|
890
|
+
{section.header && renderGroupHeader && (
|
|
891
|
+
<TableRow className="hover:bg-transparent">
|
|
892
|
+
{/* Uma célula atravessando a tabela: é o que faz a
|
|
893
|
+
faixa parecer cabeçalho, e não registro sem
|
|
894
|
+
dados. */}
|
|
895
|
+
<TableCell
|
|
896
|
+
colSpan={colSpan}
|
|
897
|
+
className="bg-muted/40 py-2"
|
|
898
|
+
>
|
|
899
|
+
{collapsibleGroups ? (
|
|
900
|
+
<button
|
|
901
|
+
type="button"
|
|
902
|
+
className="flex w-full items-center gap-3 text-left"
|
|
903
|
+
aria-expanded={!section.header.collapsed}
|
|
904
|
+
onClick={() => toggleGroup(section.key)}
|
|
905
|
+
>
|
|
906
|
+
<ChevronRight
|
|
907
|
+
className={cn(
|
|
908
|
+
"h-3.5 w-3.5 flex-none text-muted-foreground transition-transform",
|
|
909
|
+
!section.header.collapsed && "rotate-90",
|
|
910
|
+
)}
|
|
911
|
+
/>
|
|
912
|
+
{/* O conteúdo ocupa o resto: é o que deixa o
|
|
913
|
+
total encostar na borda direita, como
|
|
914
|
+
encostaria fora do botão. */}
|
|
915
|
+
<span className="min-w-0 flex-1">
|
|
916
|
+
{renderGroupHeader(section.header)}
|
|
917
|
+
</span>
|
|
918
|
+
</button>
|
|
468
919
|
) : (
|
|
469
|
-
|
|
470
|
-
// em destaque na que ordena: sem a cor, achar a
|
|
471
|
-
// coluna ativa exige comparar o desenho de todas.
|
|
472
|
-
<ArrowUpDown className="h-3.5 w-3.5 opacity-40" />
|
|
920
|
+
renderGroupHeader(section.header)
|
|
473
921
|
)}
|
|
474
|
-
</
|
|
475
|
-
|
|
476
|
-
flexRender(
|
|
477
|
-
header.column.columnDef.header,
|
|
478
|
-
header.getContext(),
|
|
479
|
-
)
|
|
480
|
-
)}
|
|
481
|
-
</TableHead>
|
|
482
|
-
);
|
|
483
|
-
})}
|
|
484
|
-
</TableRow>
|
|
485
|
-
))}
|
|
486
|
-
</TableHeader>
|
|
487
|
-
<TableBody>
|
|
488
|
-
{loading && (
|
|
489
|
-
<TableRow>
|
|
490
|
-
<TableCell colSpan={colSpan} className="h-24 text-center">
|
|
491
|
-
<Spinner className="mx-auto" />
|
|
492
|
-
</TableCell>
|
|
493
|
-
</TableRow>
|
|
494
|
-
)}
|
|
495
|
-
{!loading && table.getRowModel().rows.length === 0 && (
|
|
496
|
-
<TableRow>
|
|
497
|
-
<TableCell
|
|
498
|
-
colSpan={colSpan}
|
|
499
|
-
className="h-24 text-center text-muted-foreground"
|
|
500
|
-
>
|
|
501
|
-
{emptyText}
|
|
502
|
-
</TableCell>
|
|
503
|
-
</TableRow>
|
|
504
|
-
)}
|
|
505
|
-
{!loading &&
|
|
506
|
-
table.getRowModel().rows.map((row, index) => {
|
|
507
|
-
const dragging = draggedRow === row.id;
|
|
508
|
-
const shift = dragging ? 0 : rowShift(index);
|
|
509
|
-
|
|
510
|
-
return (
|
|
511
|
-
<TableRow
|
|
512
|
-
key={row.id}
|
|
513
|
-
data-row-id={row.id}
|
|
514
|
-
className={cn(
|
|
515
|
-
// A linha viaja com o dedo, destacada do resto da tabela —
|
|
516
|
-
// é o que diz que o arrasto pegou, e qual linha pegou.
|
|
517
|
-
dragging &&
|
|
518
|
-
"relative z-20 bg-card shadow-xl ring-1 ring-primary/50",
|
|
519
|
-
draggedRow !== null && "select-none",
|
|
520
|
-
/*
|
|
521
|
-
A transição existe só enquanto o arrasto dura, e só nas
|
|
522
|
-
linhas paradas: na que segue o dedo ela viraria atraso,
|
|
523
|
-
e depois de soltar ela animaria a volta ao zero por
|
|
524
|
-
cima da lista já reordenada — dois movimentos ao mesmo
|
|
525
|
-
tempo, que é o tremor que se vê ao largar a linha.
|
|
526
|
-
*/
|
|
527
|
-
draggedRow !== null &&
|
|
528
|
-
!dragging &&
|
|
529
|
-
"transition-transform duration-200",
|
|
922
|
+
</TableCell>
|
|
923
|
+
</TableRow>
|
|
530
924
|
)}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
: undefined
|
|
542
|
-
}
|
|
543
|
-
>
|
|
544
|
-
{reorderable && (
|
|
545
|
-
<TableCell className="w-px pr-0">
|
|
546
|
-
<button
|
|
547
|
-
type="button"
|
|
548
|
-
aria-label={t("table.reorder")}
|
|
549
|
-
title={
|
|
550
|
-
canReorder
|
|
551
|
-
? t("table.reorder")
|
|
552
|
-
: t("table.reorderSorted")
|
|
553
|
-
}
|
|
554
|
-
disabled={!canReorder}
|
|
555
|
-
// `touch-none` é o que impede a página de rolar embaixo
|
|
556
|
-
// do dedo enquanto a linha viaja.
|
|
925
|
+
|
|
926
|
+
{section.rows.map((row, indexNaSecao) => {
|
|
927
|
+
const index = section.offset + indexNaSecao;
|
|
928
|
+
const dragging = draggedRow === row.id;
|
|
929
|
+
const shift = dragging ? 0 : rowShift(index);
|
|
930
|
+
|
|
931
|
+
return (
|
|
932
|
+
<TableRow
|
|
933
|
+
key={row.id}
|
|
934
|
+
data-row-id={row.id}
|
|
557
935
|
className={cn(
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
936
|
+
// A linha viaja com o dedo, destacada do resto da tabela —
|
|
937
|
+
// é o que diz que o arrasto pegou, e qual linha pegou.
|
|
938
|
+
dragging &&
|
|
939
|
+
"relative z-20 bg-card shadow-xl ring-1 ring-primary/50",
|
|
940
|
+
draggedRow !== null && "select-none",
|
|
941
|
+
/*
|
|
942
|
+
A transição existe só enquanto o arrasto dura, e só nas
|
|
943
|
+
linhas paradas: na que segue o dedo ela viraria atraso,
|
|
944
|
+
e depois de soltar ela animaria a volta ao zero por
|
|
945
|
+
cima da lista já reordenada — dois movimentos ao mesmo
|
|
946
|
+
tempo, que é o tremor que se vê ao largar a linha.
|
|
947
|
+
*/
|
|
948
|
+
draggedRow !== null &&
|
|
949
|
+
!dragging &&
|
|
950
|
+
"transition-transform duration-200",
|
|
562
951
|
)}
|
|
563
|
-
|
|
564
|
-
|
|
952
|
+
style={
|
|
953
|
+
dragging
|
|
954
|
+
? {
|
|
955
|
+
// O `scale` é o "levantou da mesa": a linha na mão
|
|
956
|
+
// fica maior que as paradas, como o card que sai
|
|
957
|
+
// da coluna no quadro.
|
|
958
|
+
transform: `translateY(${dragOffset}px) scale(1.01)`,
|
|
959
|
+
}
|
|
960
|
+
: shift !== 0
|
|
961
|
+
? { transform: `translateY(${shift}px)` }
|
|
962
|
+
: undefined
|
|
565
963
|
}
|
|
566
|
-
onPointerMove={canReorder ? moveRowDrag : undefined}
|
|
567
|
-
onPointerUp={canReorder ? endRowDrag : undefined}
|
|
568
|
-
onPointerCancel={canReorder ? endRowDrag : undefined}
|
|
569
964
|
>
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
965
|
+
{reorderable && (
|
|
966
|
+
<TableCell className="w-px pr-0">
|
|
967
|
+
<button
|
|
968
|
+
type="button"
|
|
969
|
+
aria-label={t("table.reorder")}
|
|
970
|
+
title={
|
|
971
|
+
canReorder
|
|
972
|
+
? t("table.reorder")
|
|
973
|
+
: groupBy
|
|
974
|
+
? t("table.reorderGrouped")
|
|
975
|
+
: t("table.reorderSorted")
|
|
976
|
+
}
|
|
977
|
+
disabled={!canReorder}
|
|
978
|
+
// `touch-none` é o que impede a página de rolar embaixo
|
|
979
|
+
// do dedo enquanto a linha viaja.
|
|
980
|
+
className={cn(
|
|
981
|
+
"flex touch-none items-center text-muted-foreground",
|
|
982
|
+
canReorder
|
|
983
|
+
? "cursor-grab active:cursor-grabbing hover:text-foreground"
|
|
984
|
+
: "cursor-not-allowed opacity-30",
|
|
985
|
+
)}
|
|
986
|
+
onPointerDown={
|
|
987
|
+
canReorder ? startRowDrag(row.id) : undefined
|
|
988
|
+
}
|
|
989
|
+
onPointerMove={
|
|
990
|
+
canReorder ? moveRowDrag : undefined
|
|
991
|
+
}
|
|
992
|
+
onPointerUp={
|
|
993
|
+
canReorder ? endRowDrag : undefined
|
|
994
|
+
}
|
|
995
|
+
onPointerCancel={
|
|
996
|
+
canReorder ? endRowDrag : undefined
|
|
997
|
+
}
|
|
998
|
+
>
|
|
999
|
+
<GripVertical className="h-4 w-4" />
|
|
1000
|
+
</button>
|
|
1001
|
+
</TableCell>
|
|
1002
|
+
)}
|
|
1003
|
+
{row.getVisibleCells().map((cell) => (
|
|
1004
|
+
<TableCell
|
|
1005
|
+
key={cell.id}
|
|
1006
|
+
className={shrinkClass(cell.column.columnDef)}
|
|
1007
|
+
>
|
|
1008
|
+
{flexRender(
|
|
1009
|
+
cell.column.columnDef.cell,
|
|
1010
|
+
cell.getContext(),
|
|
1011
|
+
)}
|
|
1012
|
+
</TableCell>
|
|
1013
|
+
))}
|
|
1014
|
+
</TableRow>
|
|
1015
|
+
);
|
|
1016
|
+
})}
|
|
1017
|
+
</Fragment>
|
|
1018
|
+
))}
|
|
1019
|
+
</TableBody>
|
|
1020
|
+
|
|
1021
|
+
{temFechamento && !loading && (
|
|
1022
|
+
<TableFooter>
|
|
1023
|
+
{table.getFooterGroups().map((grupo) => (
|
|
1024
|
+
<TableRow key={grupo.id}>
|
|
1025
|
+
{/* A coluna da alça não tem fechamento, mas precisa da
|
|
1026
|
+
célula: sem ela a linha inteira sobe uma casa. */}
|
|
1027
|
+
{reorderable && <TableCell className="w-px" />}
|
|
1028
|
+
{grupo.headers.map((header) => (
|
|
575
1029
|
<TableCell
|
|
576
|
-
key={
|
|
577
|
-
className={shrinkClass(
|
|
1030
|
+
key={header.id}
|
|
1031
|
+
className={shrinkClass(header.column.columnDef)}
|
|
578
1032
|
>
|
|
579
|
-
{
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
1033
|
+
{header.isPlaceholder
|
|
1034
|
+
? null
|
|
1035
|
+
: flexRender(
|
|
1036
|
+
header.column.columnDef.footer,
|
|
1037
|
+
header.getContext(),
|
|
1038
|
+
)}
|
|
583
1039
|
</TableCell>
|
|
584
1040
|
))}
|
|
585
1041
|
</TableRow>
|
|
586
|
-
)
|
|
587
|
-
|
|
588
|
-
</TableBody>
|
|
589
|
-
</Table>
|
|
590
|
-
|
|
591
|
-
{/*
|
|
592
|
-
Três blocos, e a ordem tem motivo: quantos são à esquerda, para onde
|
|
593
|
-
ir no meio, e o tamanho da página à direita — o controle que se mexe
|
|
594
|
-
uma vez fica na ponta, e a navegação, que se usa o tempo todo, no
|
|
595
|
-
centro. Em tela estreita eles empilham em vez de espremer.
|
|
596
|
-
*/}
|
|
597
|
-
<div className="flex flex-col items-center gap-3 border-t border-border px-4 py-3 text-sm text-muted-foreground sm:flex-row sm:justify-between">
|
|
598
|
-
<span className="sm:flex-1">
|
|
599
|
-
{total} {t("table.results")}
|
|
600
|
-
</span>
|
|
601
|
-
|
|
602
|
-
<div className="flex items-center gap-1">
|
|
603
|
-
<Button
|
|
604
|
-
variant="ghost"
|
|
605
|
-
size="icon"
|
|
606
|
-
aria-label={t("table.previousPage")}
|
|
607
|
-
disabled={!table.getCanPreviousPage()}
|
|
608
|
-
onClick={() => table.previousPage()}
|
|
609
|
-
>
|
|
610
|
-
<ChevronLeft className="h-4 w-4" />
|
|
611
|
-
</Button>
|
|
612
|
-
|
|
613
|
-
{pageRange(pageIndex, pageCount).map((item, posicao) =>
|
|
614
|
-
item === "gap" ? (
|
|
615
|
-
// O buraco não é botão: `key` pela posição porque duas elipses
|
|
616
|
-
// são indistinguíveis entre si.
|
|
617
|
-
<span
|
|
618
|
-
key={`gap-${posicao}`}
|
|
619
|
-
className="px-1 text-muted-foreground"
|
|
620
|
-
>
|
|
621
|
-
…
|
|
622
|
-
</span>
|
|
623
|
-
) : (
|
|
624
|
-
<Button
|
|
625
|
-
key={item}
|
|
626
|
-
variant={item === pageIndex + 1 ? "default" : "ghost"}
|
|
627
|
-
size="icon"
|
|
628
|
-
// Quadrado de 40px como base, mas cresce com o número: a
|
|
629
|
-
// página 128 não pode vazar do botão.
|
|
630
|
-
className="w-auto min-w-10 px-2 tabular-nums"
|
|
631
|
-
aria-current={item === pageIndex + 1 ? "page" : undefined}
|
|
632
|
-
onClick={() => table.setPageIndex(item - 1)}
|
|
633
|
-
>
|
|
634
|
-
{item}
|
|
635
|
-
</Button>
|
|
636
|
-
),
|
|
1042
|
+
))}
|
|
1043
|
+
</TableFooter>
|
|
637
1044
|
)}
|
|
1045
|
+
</Table>
|
|
638
1046
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
aria-label={t("table.nextPage")}
|
|
643
|
-
disabled={!table.getCanNextPage()}
|
|
644
|
-
onClick={() => table.nextPage()}
|
|
645
|
-
>
|
|
646
|
-
<ChevronRight className="h-4 w-4" />
|
|
647
|
-
</Button>
|
|
648
|
-
</div>
|
|
649
|
-
|
|
650
|
-
<div className="sm:flex-1 sm:text-right">
|
|
651
|
-
<select
|
|
652
|
-
value={pageSize}
|
|
653
|
-
onChange={(e) => table.setPageSize(Number(e.target.value))}
|
|
654
|
-
className="h-9 rounded-md border border-input bg-transparent px-2 text-sm"
|
|
655
|
-
>
|
|
656
|
-
{pageSizeOptions.map((n) => (
|
|
657
|
-
<option key={n} value={n}>
|
|
658
|
-
{n} {t("table.perPage")}
|
|
659
|
-
</option>
|
|
660
|
-
))}
|
|
661
|
-
</select>
|
|
662
|
-
</div>
|
|
663
|
-
</div>
|
|
664
|
-
</Card>
|
|
1047
|
+
{!semRodape && rodape}
|
|
1048
|
+
</Card>
|
|
1049
|
+
)}
|
|
665
1050
|
|
|
666
1051
|
{storageKey && (
|
|
667
1052
|
<ColumnVisibilityModal
|