@snksergio/design-system 0.61.0 → 0.62.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/dist-lib/ai/componentes/AlertModal.md +47 -0
- package/dist-lib/ai/componentes/AppShell.md +117 -0
- package/dist-lib/ai/componentes/Breadcrumb.md +187 -0
- package/dist-lib/ai/componentes/Button.md +116 -0
- package/dist-lib/ai/componentes/ButtonGroup.md +162 -0
- package/dist-lib/ai/componentes/CardCheckbox.md +99 -0
- package/dist-lib/ai/componentes/CardOption.md +133 -0
- package/dist-lib/ai/componentes/Chart.md +93 -0
- package/dist-lib/ai/componentes/Chip.md +68 -0
- package/dist-lib/ai/componentes/ChoroplethMap.md +118 -0
- package/dist-lib/ai/componentes/ColorPicker.md +70 -0
- package/dist-lib/ai/componentes/Combobox.md +51 -0
- package/dist-lib/ai/componentes/ConversationListItem.md +90 -0
- package/dist-lib/ai/componentes/DataList.md +111 -0
- package/dist-lib/ai/componentes/DataTable.md +867 -0
- package/dist-lib/ai/componentes/DatePicker.md +84 -0
- package/dist-lib/ai/componentes/DateSeparatorChip.md +59 -0
- package/dist-lib/ai/componentes/EmptyState.md +72 -0
- package/dist-lib/ai/componentes/FileUploadField.md +95 -0
- package/dist-lib/ai/componentes/FloatingPanel.md +118 -0
- package/dist-lib/ai/componentes/FooterTable.md +62 -0
- package/dist-lib/ai/componentes/FormField.md +110 -0
- package/dist-lib/ai/componentes/Gantt.md +552 -0
- package/dist-lib/ai/componentes/Header.md +98 -0
- package/dist-lib/ai/componentes/Icon.md +65 -0
- package/dist-lib/ai/componentes/Kanban.md +343 -0
- package/dist-lib/ai/componentes/Kpi.md +103 -0
- package/dist-lib/ai/componentes/List.md +61 -0
- package/dist-lib/ai/componentes/MarkdownText.md +59 -0
- package/dist-lib/ai/componentes/MenuSidebar.md +128 -0
- package/dist-lib/ai/componentes/MessageAck.md +53 -0
- package/dist-lib/ai/componentes/MessageBubble.md +115 -0
- package/dist-lib/ai/componentes/MessageComposer.md +80 -0
- package/dist-lib/ai/componentes/MessageVariablesPicker.md +104 -0
- package/dist-lib/ai/componentes/Modal.md +88 -0
- package/dist-lib/ai/componentes/MonthYearPicker.md +49 -0
- package/dist-lib/ai/componentes/PageHeader.md +129 -0
- package/dist-lib/ai/componentes/Panel.md +84 -0
- package/dist-lib/ai/componentes/Scheduler.md +421 -0
- package/dist-lib/ai/componentes/ScreenLoader.md +60 -0
- package/dist-lib/ai/componentes/SingleMenuSidebar.md +171 -0
- package/dist-lib/ai/componentes/Spinner.md +52 -0
- package/dist-lib/ai/componentes/Table.md +192 -0
- package/dist-lib/ai/componentes/TableToolbar.md +87 -0
- package/dist-lib/ai/componentes/TabsNavigation.md +152 -0
- package/dist-lib/ai/componentes/Toast.md +49 -0
- package/dist-lib/ai/componentes/_primitivos.md +74 -0
- package/dist-lib/ai/componentes/avatar-ig.md +181 -0
- package/dist-lib/ai/componentes/indice.json +49 -0
- package/dist-lib/ai/exemplos/dashboard/dashboard-brazil-map.ts +33 -0
- package/dist-lib/ai/exemplos/dashboard/dashboard-screen.tsx +1110 -0
- package/dist-lib/ai/exemplos/dashboard/index.ts +1 -0
- package/dist-lib/ai/global/componentes.md +217 -0
- package/dist-lib/ai/global/composicao.md +182 -0
- package/dist-lib/ai/indice.json +177 -0
- package/dist-lib/ai/lint/ds-lint-patterns.mjs +115 -0
- package/dist-lib/ai/manifest.json +16 -0
- package/dist-lib/ai/regras/design.md +88 -0
- package/dist-lib/ai/regras/temas.md +192 -0
- package/dist-lib/ai/regras-por-componente.json +103 -0
- package/dist-lib/ai/roteiros/dashboard/blueprint.md +47 -0
- package/dist-lib/ai/roteiros/dashboard/entrevista.md +62 -0
- package/dist-lib/ai/roteiros/dashboard/geracao.md +88 -0
- package/dist-lib/ai/roteiros/dashboard/roteiro.md +88 -0
- package/package.json +4 -1
|
@@ -0,0 +1,867 @@
|
|
|
1
|
+
# DataTable — Guia de uso
|
|
2
|
+
|
|
3
|
+
<!-- ds:regras
|
|
4
|
+
- filtro é NATIVO e reativo: `enableColumnFilter` na coluna — nunca select/form solto acima da grade. Pra ele aparecer desde o load (e não ficar escondido atrás do ícone), `showEmptyFilterChips={["status", …]}`
|
|
5
|
+
- não fixe `width`: com `autoFit` (default) ele é PISO e entra no rateio, não trava. Travar de verdade = `width` + `maxWidth` iguais
|
|
6
|
+
- vazio são DOIS casos distintos: sem dado nenhum → `renderEmpty` (CTA de criar); filtro/busca zerou → `renderNoResults` (o "limpar filtros" já vem cabeado)
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
Wrapper smart sobre `<TableToolbar>` + `<Table>` + `<FooterTable>` que orquestra **17 hooks SRP** (sort, filter, search, pagination, selection, visibility, density, processor, query, export, saved views, persistence, etc) e renderiza body com suporte a virtualização, agrupamento e expansão.
|
|
10
|
+
|
|
11
|
+
> **Princípio**: o DataTable é smart, mas cada primitive (Table, TableToolbar, FooterTable) é dumb e standalone. Veja `Table/USAGE.md` e `TableToolbar/USAGE.md` se quiser montar uma tabela custom fora do DataTable.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Imports
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import {
|
|
19
|
+
DataTable,
|
|
20
|
+
type DataTableColumnDef,
|
|
21
|
+
type DataTableRef,
|
|
22
|
+
// builders pra reduzir boilerplate:
|
|
23
|
+
textColumn,
|
|
24
|
+
currencyColumn,
|
|
25
|
+
dateColumn,
|
|
26
|
+
statusColumn,
|
|
27
|
+
actionColumn,
|
|
28
|
+
// registry pra tipos custom:
|
|
29
|
+
columnTypeRegistry,
|
|
30
|
+
type ColumnTypeDefinition,
|
|
31
|
+
} from "@/components/ui/DataTable";
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Quick start — client mode (CRUD)
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
interface Client {
|
|
40
|
+
id: number;
|
|
41
|
+
name: string;
|
|
42
|
+
email: string;
|
|
43
|
+
status: "active" | "inactive";
|
|
44
|
+
value: number;
|
|
45
|
+
createdAt: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ⚠️ Nenhuma coluna fixa `width` de propósito: com `autoFit` (default) o DataTable
|
|
49
|
+
// mede o conteúdo e distribui o espaço. `width` aqui seria PISO, não trava — fixar
|
|
50
|
+
// em todas só desloca o ponto de partida do rateio. Trave uma coluna só quando
|
|
51
|
+
// precisar, com `width` + `maxWidth` iguais.
|
|
52
|
+
const columns = useMemo<DataTableColumnDef<Client>[]>(
|
|
53
|
+
() => [
|
|
54
|
+
textColumn<Client>("id", "ID"),
|
|
55
|
+
textColumn<Client>("name", "Nome", { sortable: true }),
|
|
56
|
+
{ field: "email", headerName: "Email", type: "email" },
|
|
57
|
+
currencyColumn<Client>("value", "Valor", { currency: "BRL" }),
|
|
58
|
+
dateColumn<Client>("createdAt", "Criado em"),
|
|
59
|
+
statusColumn<Client>("status", "Status", [
|
|
60
|
+
{ value: "active", label: "Ativo", color: "success" },
|
|
61
|
+
{ value: "inactive", label: "Inativo", color: "muted" },
|
|
62
|
+
]),
|
|
63
|
+
actionColumn<Client>({
|
|
64
|
+
getActions: ({ row }) => [
|
|
65
|
+
{ label: "Editar", onClick: () => editClient(row) },
|
|
66
|
+
{
|
|
67
|
+
label: "Excluir",
|
|
68
|
+
onClick: () => removeClient(row),
|
|
69
|
+
destructive: true,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
}),
|
|
73
|
+
],
|
|
74
|
+
[editClient, removeClient],
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
<DataTable<Client>
|
|
78
|
+
rows={clients}
|
|
79
|
+
columns={columns}
|
|
80
|
+
toolbar={{ title: "Clientes", enableSearch: true, enableFilters: true }}
|
|
81
|
+
paginationConfig={{ enabled: true, initialPageSize: 25 }}
|
|
82
|
+
selectionConfig={{ enabled: true, enableGlobal: true }}
|
|
83
|
+
onRowClick={(row) => router.push(`/clients/${row.id}`)}
|
|
84
|
+
/>;
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
> `columns` **deve** ser memoizado — o processor reage à identidade do array, não ao conteúdo.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Capacidades
|
|
92
|
+
|
|
93
|
+
| Capability | Como ativar |
|
|
94
|
+
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
95
|
+
| **Sort multi** | `sortable: true` na coluna; toolbar Sort popover surge automaticamente |
|
|
96
|
+
| **Filter chip rápido** | `enableColumnFilter: true` + `filterType: "text"\|"number"\|"date"\|"select"\|"multiSelect"\|"boolean"` |
|
|
97
|
+
| **Filter avançado (AND/OR)** | Habilitado por default se houver coluna com `enableColumnFilter` |
|
|
98
|
+
| **Filter chips placeholder** | `showEmptyFilterChips={["status", "categoria"]}` — chips nativos visíveis desde o load inicial, mesmo sem valor preenchido (user clica e preenche) |
|
|
99
|
+
| **Search global** | `toolbar.enableSearch: true` (default). Client mode busca em todos os fields; server mode recebe `search` (debounced) + `searchField?` no `GridFetchParams` |
|
|
100
|
+
| **Pagination** | `paginationConfig.enabled: true` (default) |
|
|
101
|
+
| **Selection (bulk)** | `selectionConfig.enabled: true` |
|
|
102
|
+
| **Visibility / pin / reorder** | `toolbar.enableColumns: true` (default) |
|
|
103
|
+
| **Density toggle** | `toolbar.enableDensity: true` (default). Override items via `densityItems` prop |
|
|
104
|
+
| **Column types registry** | `type: "currency"` etc — renderiza display + filter input via registry |
|
|
105
|
+
| **Formato de data** | `type: "date"` → `14/03/2023` · `type: "datetime"` → `14/03/2023 09:30`. Outro formato? **`valueFormatter`** — ele vence o do tipo e vale na célula, no export e no clipboard. Não precisa de `render`. ⚠️ Até a v0.42.1 esses tipos formatavam **sem ano** ("14 de mar") e o `valueFormatter` **não alcançava a célula** (mudava só export/totalizador). |
|
|
106
|
+
| **Coluna de ações** | `type: "actions"` + `getActions` (ou o builder `actionColumn`). **É o `type` que dá as 3 garantias** — última coluna, ancorada à direita, largura fixa. Coluna montada na unha com botões não recebe nenhuma delas. Ver §Coluna de ações abaixo. |
|
|
107
|
+
| **Inline edit** | `editable: true` na coluna + `onCellEditCommit` |
|
|
108
|
+
| **Read-more (Ler mais)** | `readMore: true` na coluna (ou `{ lines?, label? }`) — trunca + popover com texto completo |
|
|
109
|
+
| **Copy célula** | `copyable: true` na coluna (ou `{ value?, label? }`) — ícone copiar no hover + feedback "Copiado!" (~2s) |
|
|
110
|
+
| **Grab-to-scroll horizontal** | **nativo (default `true`)** — arrastar o corpo (mouse/pen) rola lateralmente; `grabToScroll={false}` desliga |
|
|
111
|
+
| **Tela cheia (fullscreen)** | `toolbar.enableFullscreen: true` — botão ⤢ na toolbar expande a tabela pra viewport inteira (Esc volta) |
|
|
112
|
+
| **Ações custom no toolbar** | `toolbar.actions: ToolbarAction[]` — `button`/`dropdown`/`input` (ex.: seletor de período). Inline no desktop (entre Filtros e ⋯); no mobile colapsam num ⋯ próprio. Ver `<ToolbarActions>` no TableToolbar |
|
|
113
|
+
| **Server mode** | passe `fetchData` em vez de `rows` |
|
|
114
|
+
| **Card responsivo (mobile)** | `cardBreakpoint` (default 768). Abaixo desse valor o **default é tabela** (densidade > cards pra power user); o usuário alterna pra cards via toggle **"Exibição" (Linhas/Cards)** que aparece na ToolbarSettingsMenu (`mobileDisplayToggle`). `cardBreakpoint={false}` desabilita o card mode por completo. |
|
|
115
|
+
| **Toolbar responsiva (mobile)** | Em viewports `<md` (768px), controles secundários (sort / cols / density / refresh / view toggle / saved views / export / more menu) colapsam automaticamente num icon-button dropdown `...` via `ToolbarMobileDialog`. Search e Filter continuam sempre visíveis na linha principal. Comportamento built-in — sem prop necessária. |
|
|
116
|
+
| **Virtualização** | `virtualize: true` (+ `estimateRowHeight` / `overscan` opcionais) |
|
|
117
|
+
| **Row grouping** | `groupBy: "status"` (1 field na V1) + opcionais `renderGroupHeader`/`renderGroupContent` pra free-form |
|
|
118
|
+
| **Row expansion** | `expandable: true` na coluna + `renderRowExpansion: ({ row }) => <Detail row={row} />` |
|
|
119
|
+
| **Tree-data (hierarquia)** | `getTreeDataPath: (row) => [...]` + `treeColumn: true` na coluna primária. Rows continuam FLAT; o path define a árvore. Pagination desliga automaticamente. |
|
|
120
|
+
| **Saved views** | `savedViewsService` (use `savedViewsMockService` em dev) |
|
|
121
|
+
| **State persistence** | `persistId: "clients-table"` — workspace "Default" completo persiste em localStorage (sort, filter, search, page, density, column widths/pin/hide/order, viewMode, groupBy, expanded rows). Quando view custom está ativa, o snapshot da Default fica congelado — voltar para Default restaura tudo intacto. Limpeza manual via `ref.current.resetPersistedState()`. |
|
|
122
|
+
| **Auto-fit das colunas** | `autoFit: true` (default) — observa container via ResizeObserver, mede conteúdo das primeiras N rows (canvas) e distribui a sobra. ⚠️ **`col.width` é PISO, não trava** — a coluna entra no rateio e cresce a partir dele (medido: pedir 80/240/280 num container de 1400px devolve 187/560/653). Pra travar de verdade: **`width` + `maxWidth` iguais**. Prefira não fixar `width`. `autoFit={false}` desliga (legacy). |
|
|
123
|
+
| **Resize manual de colunas** | Default ativo em todas as colunas exceto `type: "actions"` ou `purpose: "selection"`. Drag handle aparece no edge direito do header. Limites hard `60–800px`; respeita `col.minWidth/maxWidth` quando definidos. Para desabilitar em uma coluna específica: `resizable: false`. |
|
|
124
|
+
| **Export** | `toolbar.enableExport: true` (CSV default com escopos all/filtered/selected) — formatos custom via `enableExport: { formats: [{ id, label, onSelect }] }` |
|
|
125
|
+
| **View Kanban (board)** | `viewMode="kanban"` (controlled) ou `defaultViewMode` (uncontrolled) + `kanbanConfig={{ groupByField, renderCard }}` — toggle table/kanban auto na toolbar |
|
|
126
|
+
| **View Lista (cards)** | `viewMode="list"` + `listConfig={{ renderItem(row) }}` — toggle Tabela/Lista auto na toolbar; mesma toolbar, corpo vira `<List>`. `hierarchical: true` + `getTreeDataPath` = lista em árvore. Showcase `#/clients-list-view` |
|
|
127
|
+
| **Totalizer row** | `showTotalizers` na DataTable + `aggregate: "sum"` (+ `aggregateFormatter`) na coluna; server mode pode sobrescrever via `aggregateRow` |
|
|
128
|
+
| **Keyboard navigation** | Auto — setas, Home/End, PgUp/PgDn no body |
|
|
129
|
+
| **Estados (vazio / carregando / sem resultado)** | Já vêm com default embutido; `loading: boolean` + `renderEmpty` / `renderLoading` / `renderNoResults` **substituem** (são `ReactNode`, não função). Ver §Estados abaixo |
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Receitas comuns
|
|
134
|
+
|
|
135
|
+
### Estados: vazio, carregando, sem resultado
|
|
136
|
+
|
|
137
|
+
As três telas que aparecem quando não há linha pra mostrar. **Os três já têm default do
|
|
138
|
+
DS** — você só passa a prop pra substituir 100% do slot:
|
|
139
|
+
|
|
140
|
+
| Prop | Dispara quando | Default embutido |
|
|
141
|
+
|---|---|---|
|
|
142
|
+
| `renderLoading` | `loading` é `true` | spinner do DS |
|
|
143
|
+
| `renderEmpty` | o dataset não tem **nenhum** registro | `<DataTableEmpty />` — ilustração + título + descrição + ação opcional |
|
|
144
|
+
| `renderNoResults` | há linhas, mas **filtro/busca zerou** o resultado | `<DataTableNoResults />` com **`onClearFilters` já cabeado** pelo DataTable (limpa `filterModel` + `search`) |
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
<DataTable
|
|
148
|
+
rows={rows}
|
|
149
|
+
columns={columns}
|
|
150
|
+
loading={isFetching} // prop SEPARADA, boolean
|
|
151
|
+
renderLoading={<MeuSkeleton />} // ReactNode — não é função, não recebe props
|
|
152
|
+
renderEmpty={<EmptyState title="Nenhum cliente ainda" action={<Button>Novo</Button>} />}
|
|
153
|
+
renderNoResults={<EmptyState title="Nada encontrado" description="Ajuste os filtros." />}
|
|
154
|
+
/>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
⚠️ **Empty ≠ NoResults, e trocar os dois é o erro comum.** "Não existe nada ainda" pede
|
|
158
|
+
CTA de **criar**; "seu filtro não achou" pede **limpar filtro** — e nesse segundo caso o
|
|
159
|
+
default já entrega o botão certo, cabeado. Substituir por um `EmptyState` genérico
|
|
160
|
+
**perde** esse wiring: se for substituir o `renderNoResults`, cabeie você mesmo o clear.
|
|
161
|
+
|
|
162
|
+
### Coluna de ações (editar / excluir / "…")
|
|
163
|
+
|
|
164
|
+
**Use `type: "actions"`.** É o `type` — não a posição no array, não `pinned` — que liga as
|
|
165
|
+
três garantias, todas resolvidas no `use-data-table-columns.ts`:
|
|
166
|
+
|
|
167
|
+
1. **Vai pro fim** da tabela, mesmo se você declarar a coluna no meio do array.
|
|
168
|
+
2. **Ancora à direita** (`pinned: "right"` implícito) — fica visível com scroll horizontal.
|
|
169
|
+
Passar `pinned: "right"` na mão é redundante.
|
|
170
|
+
3. **Não entra no rateio do autoFit** — largura fixa, não estica junto das outras.
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
// builder (recomendado) — `field`/`headerName`/`type`/`pinned` já vêm certos
|
|
174
|
+
actionColumn<Client>({
|
|
175
|
+
getActions: ({ row }) => [
|
|
176
|
+
{ id: "edit", label: "Editar", icon: <Pencil />, onClick: () => edit(row) },
|
|
177
|
+
{ id: "del", label: "Excluir", icon: <Trash2 />, destructive: true, onClick: () => del(row) },
|
|
178
|
+
],
|
|
179
|
+
}),
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
⛔ **Montar a coluna na unha perde as três.** `{ field: "acoes", headerName: "Ações",
|
|
183
|
+
render: () => <><Button/><Button/></> }` não tem `type`, então: não vai pro fim, não ancora,
|
|
184
|
+
**e entra no rateio** — medido, uma coluna assim com `width: 120` num container de 1400px
|
|
185
|
+
termina com **220px**, e o conteúdo alinhado à esquerda deixa os botões ~100px longe da
|
|
186
|
+
borda. É o sintoma "o botão de ação ficou no meio da tabela". Se precisa de render próprio,
|
|
187
|
+
mantenha `type: "actions"` e use `getActions` — `customColumn` **não** serve pra isso.
|
|
188
|
+
|
|
189
|
+
#### Quantas ações aparecem inline
|
|
190
|
+
|
|
191
|
+
| ações visíveis na row | render | largura |
|
|
192
|
+
|---|---|---|
|
|
193
|
+
| 1 | 1 ícone, sem "…" | 44px |
|
|
194
|
+
| 2 | 2 ícones | 74px |
|
|
195
|
+
| 3 | 3 ícones | 104px |
|
|
196
|
+
| **4+** | **só o "…"**, todas dentro | 44px |
|
|
197
|
+
| qualquer nº, com `showInMenu` em algum item | **o seu split**, sem limite | derivada |
|
|
198
|
+
|
|
199
|
+
O corte em 3 é a **capacidade geométrica** da coluna, não preferência: a célula usa
|
|
200
|
+
`px-pad-md` na variante `actions` (8×2 — sobrescreve o `px-pad-2xl` das outras) + ícone
|
|
201
|
+
`icon-2xs` (28px) + gap `gp-2xs` (2px) → `largura = 30n + 14`. Quatro ícones pedem 134px,
|
|
202
|
+
contra os 120 que a coluna sempre teve. Antes da v0.42.0 a largura era 120px **fixos** pra
|
|
203
|
+
qualquer quantidade: 1 ação reservava espaço pra 3, e 4 ícones vazavam a coluna.
|
|
204
|
+
|
|
205
|
+
Pra forçar um split diferente, marque `showInMenu: true` nos itens que devem ir pro menu —
|
|
206
|
+
isso desliga o automático e respeita você integralmente, inclusive com 5 ícones inline.
|
|
207
|
+
|
|
208
|
+
`hidden` é resolvido **por row antes de contar**: uma row com 4 ações onde uma está oculta
|
|
209
|
+
volta a renderizar 3 ícones inline. A largura reservada é o **máximo** entre as rows
|
|
210
|
+
amostradas, senão a row mais completa ficaria cortada.
|
|
211
|
+
|
|
212
|
+
**Largura:** `col.width` > `col.minWidth` > derivada da contagem. ⚠️ Até a v0.42.0 o
|
|
213
|
+
`col.width` era **ignorado** nesta coluna (só `minWidth` funcionava) — se você tem
|
|
214
|
+
`actionColumn({ width: 64 })` em código antigo, ele passa a valer de verdade agora, e 64px
|
|
215
|
+
comporta **1** ícone.
|
|
216
|
+
|
|
217
|
+
### Server mode (refetch async + paginação remota)
|
|
218
|
+
|
|
219
|
+
```tsx
|
|
220
|
+
const fetchData = useCallback(
|
|
221
|
+
async ({ pagination, sort, filters, search }: GridFetchParams) => {
|
|
222
|
+
const res = await api.get("/clients", {
|
|
223
|
+
params: serialize({ pagination, sort, filters, search }),
|
|
224
|
+
});
|
|
225
|
+
return { data: res.data.items, total: res.data.total }; // GridFetchResult<T>
|
|
226
|
+
},
|
|
227
|
+
[],
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
<DataTable<Client>
|
|
231
|
+
fetchData={fetchData}
|
|
232
|
+
columns={columns}
|
|
233
|
+
toolbar={{ enableSearch: true, enableFilters: true }}
|
|
234
|
+
paginationConfig={{ enabled: true, initialPageSize: 25 }}
|
|
235
|
+
/>;
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
`fetchData` é re-disparado quando muda `pagination | sort | filter | search`. Use ref/AbortController interno se precisar cancelar. Loading state é managed pelo controller (skeleton no body).
|
|
239
|
+
|
|
240
|
+
### Inline edit (commit no submit / Enter)
|
|
241
|
+
|
|
242
|
+
```tsx
|
|
243
|
+
const columns: DataTableColumnDef<Client>[] = [
|
|
244
|
+
{ field: "name", headerName: "Nome", editable: true, sortable: true },
|
|
245
|
+
// ...
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
<DataTable<Client>
|
|
249
|
+
rows={clients}
|
|
250
|
+
columns={columns}
|
|
251
|
+
onCellEditCommit={async ({ id, field, value, oldValue, row }) => {
|
|
252
|
+
await api.patch(`/clients/${id}`, { [field]: value });
|
|
253
|
+
refreshClients();
|
|
254
|
+
}}
|
|
255
|
+
/>;
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Double-click numa cell `editable` → input inline; Enter commita; Esc cancela; loading bloqueia outras edições.
|
|
259
|
+
|
|
260
|
+
### Mobile auto-switch para card
|
|
261
|
+
|
|
262
|
+
Por default, viewports `< 768px` rendem cada row como `<TableCardRow>` no lugar de `<TableRow>`. O toolbar (search/filter/sort) e o footer (paginação) continuam intactos — só o body que troca.
|
|
263
|
+
|
|
264
|
+
```tsx
|
|
265
|
+
<DataTable<Client>
|
|
266
|
+
rows={clients}
|
|
267
|
+
columns={columns}
|
|
268
|
+
cardBreakpoint={768} // default — abaixo deste pixel, vira card
|
|
269
|
+
// cardBreakpoint={false} // desabilita o auto-switch (mantém table sempre)
|
|
270
|
+
// cardBreakpoint={640} // breakpoint custom
|
|
271
|
+
/>
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**Mapeamento automático das colunas → card:**
|
|
275
|
+
|
|
276
|
+
- Coluna `isPrimary: true` (ou primeira coluna não-actions) → vai pro **header** do card como título
|
|
277
|
+
- Coluna `type="actions"` → vai pro **headerActions** (canto sup. direito)
|
|
278
|
+
- Checkbox de selection → vai pro header (esquerda do título)
|
|
279
|
+
- Demais colunas visíveis → viram `items` label/value no body do card
|
|
280
|
+
|
|
281
|
+
**Pra eleger qual coluna é o título do card:**
|
|
282
|
+
|
|
283
|
+
```tsx
|
|
284
|
+
const columns = [
|
|
285
|
+
{ field: "id", headerName: "ID", ... },
|
|
286
|
+
{ field: "name", headerName: "Nome", isPrimary: true, ... }, // ← vira título do card
|
|
287
|
+
// ...
|
|
288
|
+
];
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**Degradações intencionais no card mode** (silenciosas — não quebram):
|
|
292
|
+
|
|
293
|
+
- Virtualização desligada (renderiza `rowsToRender` integral, paginação ainda limita)
|
|
294
|
+
- Row expansion / Inline editing / Column resize → desativados (sem sentido em card vertical)
|
|
295
|
+
- Group rows → ainda não suportadas (TODO futuro)
|
|
296
|
+
|
|
297
|
+
### Virtualização (10k+ linhas)
|
|
298
|
+
|
|
299
|
+
```tsx
|
|
300
|
+
<DataTable<Client>
|
|
301
|
+
rows={tenThousandClients}
|
|
302
|
+
columns={columns}
|
|
303
|
+
virtualize
|
|
304
|
+
estimateRowHeight={56} // opcional — default deriva da density (40/56/64)
|
|
305
|
+
overscan={10} // opcional — rows extras fora da viewport
|
|
306
|
+
paginationConfig={{ enabled: false }} // virtualização geralmente exclui paginação
|
|
307
|
+
/>
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Usa `@tanstack/react-virtual`. Sticky header e seleção mantêm-se. Performance fica linear até ~100k rows. Requer container com altura definida (`flex-1 min-h-0` ou height fixa).
|
|
311
|
+
|
|
312
|
+
### Row grouping
|
|
313
|
+
|
|
314
|
+
```tsx
|
|
315
|
+
<DataTable<Client>
|
|
316
|
+
rows={clients}
|
|
317
|
+
columns={columns}
|
|
318
|
+
groupBy="status" // controlled (string, 1 field na V1) — ou defaultGroupBy uncontrolled
|
|
319
|
+
onGroupByChange={setGroupBy}
|
|
320
|
+
// Default (sem overrides): header column-aligned com chevron + label + count + subtotals.
|
|
321
|
+
// Free-form: passe os 2 overrides abaixo.
|
|
322
|
+
renderGroupHeader={({ group, toggle }) => (
|
|
323
|
+
<span onClick={toggle}>
|
|
324
|
+
{group.label} ({group.count})
|
|
325
|
+
</span>
|
|
326
|
+
)}
|
|
327
|
+
renderGroupContent={({ group }) => <CardsGrid rows={group.rows} />}
|
|
328
|
+
/>
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Pagination é desligada **automaticamente** quando `groupBy` está ativo. Sem os overrides, o default é column-aligned (mantém grid layout); com `renderGroupHeader`/`renderGroupContent`, o grupo vira free-form (full-width, ideal pra hierarquias complexas). Referência: `src/preview/pages/ClientsGroupedPreview.tsx` (2 modos).
|
|
332
|
+
|
|
333
|
+
### Row expansion (painel detalhe)
|
|
334
|
+
|
|
335
|
+
```tsx
|
|
336
|
+
const columns = [
|
|
337
|
+
{ field: "id", headerName: "ID", expandable: true }, // ← chevron + click trigger
|
|
338
|
+
// ...
|
|
339
|
+
];
|
|
340
|
+
|
|
341
|
+
<DataTable<Client>
|
|
342
|
+
rows={clients}
|
|
343
|
+
columns={columns}
|
|
344
|
+
renderRowExpansion={({ row }) => <ClientDetailPanel client={row} />}
|
|
345
|
+
singleExpand // opcional — default false (múltiplas rows abertas)
|
|
346
|
+
/>;
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
O chevron aparece na coluna marcada `expandable: true`. Controlled opcional via `expandedRowIds` + `onExpandedRowIdsChange` (ou `defaultExpandedRowIds` uncontrolled). Mutuamente exclusivo com `groupBy` (groupBy tem precedência). Referência: `src/preview/pages/ClientsExpandablePreview.tsx`.
|
|
350
|
+
|
|
351
|
+
### Tree-data (hierarquia multi-nível)
|
|
352
|
+
|
|
353
|
+
Hierarquia tipo AG Grid: cada linha continua **FLAT** em `rows` e o **caminho** (`getTreeDataPath`) define a árvore. O DataTable reconstrói os níveis a partir dos caminhos e renderiza indentação + chevron na coluna primária.
|
|
354
|
+
|
|
355
|
+
```tsx
|
|
356
|
+
const columns = [
|
|
357
|
+
{ field: "name", headerName: "Licenciado", treeColumn: true }, // ← coluna primária da árvore
|
|
358
|
+
// ...
|
|
359
|
+
];
|
|
360
|
+
|
|
361
|
+
// O path sobe a cadeia de patrocinador até a raiz: ["L-001", "L-010", "L-100"]
|
|
362
|
+
const byId = new Map(rows.map((r) => [r.id, r]));
|
|
363
|
+
const getTreeDataPath = (row: NetworkRow): string[] => {
|
|
364
|
+
const path: string[] = [];
|
|
365
|
+
let cur: NetworkRow | undefined = row;
|
|
366
|
+
while (cur) {
|
|
367
|
+
path.unshift(cur.id);
|
|
368
|
+
cur = cur.parentId ? byId.get(cur.parentId) : undefined;
|
|
369
|
+
}
|
|
370
|
+
return path;
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
<DataTable<NetworkRow>
|
|
374
|
+
rows={rows} // ← FLAT (não aninhadas)
|
|
375
|
+
columns={columns}
|
|
376
|
+
getRowId={(r) => r.id}
|
|
377
|
+
getTreeDataPath={getTreeDataPath}
|
|
378
|
+
treeData={{
|
|
379
|
+
defaultExpanded: true, // árvore começa aberta (default true)
|
|
380
|
+
showDescendantCount: true, // mostra "(N)" descendentes ao lado do nome
|
|
381
|
+
}}
|
|
382
|
+
/>;
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Regras:
|
|
386
|
+
|
|
387
|
+
- `getTreeDataPath(row)` retorna o array do caminho (`[raiz, ..., self]`). Linhas com path vazio são ignoradas da árvore.
|
|
388
|
+
- Se **nenhuma** coluna marcar `treeColumn: true`, o DataTable usa a primeira coluna não-`actions`.
|
|
389
|
+
- Estado de expansão reusa a máquina de row-expansion (`expandedRowIds` / `defaultExpandedRowIds` / `onExpandedRowIdsChange`). O Set guarda os ids que **divergem** do `defaultExpanded`.
|
|
390
|
+
- **Pagination desliga automaticamente** (paginar cortaria ramos). Não suportado em server mode — passe todas as rows do escopo + `virtualize` se necessário.
|
|
391
|
+
- Precedência quando mais de um modo é passado: `groupBy` > `getTreeDataPath` > `renderRowExpansion`.
|
|
392
|
+
- Search/sort operam sobre as rows; a árvore é reconstruída do resultado.
|
|
393
|
+
- **Expand-all / collapse-all** programático via imperative ref: `ref.current.expandAllTree()` / `ref.current.collapseAllTree()` (ver seção [Imperative ref](#imperative-ref)). No-op fora do modo tree-data. Respeita `treeData.defaultExpanded` e opera sobre todas as rows pós-filtro/sort. O DS não embute botões na toolbar — o consumer fia os botões e chama via ref.
|
|
394
|
+
|
|
395
|
+
```tsx
|
|
396
|
+
const tableRef = useRef<DataTableRef>(null);
|
|
397
|
+
|
|
398
|
+
<button onClick={() => tableRef.current?.expandAllTree()}>Expandir tudo</button>
|
|
399
|
+
<button onClick={() => tableRef.current?.collapseAllTree()}>Recolher tudo</button>
|
|
400
|
+
|
|
401
|
+
<DataTable<NetworkRow> ref={tableRef} getTreeDataPath={getTreeDataPath} /* ... */ />
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Referência: `src/preview/pages/ClientsTreePreview.tsx`.
|
|
405
|
+
|
|
406
|
+
### Polish de célula — Read-more (Ler mais)
|
|
407
|
+
|
|
408
|
+
Trunca conteúdo longo e abre o texto completo num popover ao clicar em "Ler mais". Equivalente DS do `ReadMoreCell` legado (que usava tooltip).
|
|
409
|
+
|
|
410
|
+
```tsx
|
|
411
|
+
const columns = [
|
|
412
|
+
// 1 linha + reticências + gatilho "Ler mais" (default)
|
|
413
|
+
{ field: "obs", headerName: "Observação", readMore: true },
|
|
414
|
+
// N linhas antes de truncar + label custom
|
|
415
|
+
{
|
|
416
|
+
field: "bio",
|
|
417
|
+
headerName: "Bio",
|
|
418
|
+
readMore: { lines: 2, label: "Ver tudo" },
|
|
419
|
+
},
|
|
420
|
+
];
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
- `readMore: true` → 1 linha, label "Ler mais". `readMore: { lines?, label? }` customiza.
|
|
424
|
+
- Desativa o `ellipsis` da cell automaticamente (a add-on gerencia o próprio truncate).
|
|
425
|
+
- Aplica-se ao render default **ou** ao `render` custom (o nó é envolvido). Ignorado em `type: "actions"`, células em edição e na coluna primária de tree-data.
|
|
426
|
+
- O texto do popover deriva do `valueFormatter`/`formatValue`/value (string) — pra HTML rico, passe um `render` que retorna o nó; ele é exibido no popover.
|
|
427
|
+
|
|
428
|
+
### Polish de célula — Copy (copiar valor)
|
|
429
|
+
|
|
430
|
+
Ícone de copiar revelado no hover/foco da célula, com feedback "Copiado!" por ~2s. Usa `navigator.clipboard` — **sem dependência nova**.
|
|
431
|
+
|
|
432
|
+
```tsx
|
|
433
|
+
const columns = [
|
|
434
|
+
// copia o texto renderizado da célula
|
|
435
|
+
{ field: "email", headerName: "E-mail", copyable: true },
|
|
436
|
+
// copia um valor derivado da row (ex: id puro) + aria-label custom
|
|
437
|
+
{
|
|
438
|
+
field: "doc",
|
|
439
|
+
headerName: "CPF",
|
|
440
|
+
copyable: { value: (row) => row.cpfRaw, label: "Copiar CPF" },
|
|
441
|
+
},
|
|
442
|
+
];
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
- `copyable: true` → copia o texto da célula. `copyable: { value?, label? }` customiza: `value` aceita string ou `(row) => string`; `label` é o aria-label/title do botão.
|
|
446
|
+
- O ícone só aparece no hover/foco (não polui a célula). O click não dispara `onRowClick`/seleção.
|
|
447
|
+
- `readMore` tem precedência: se ambos forem definidos na mesma coluna, vale `readMore`.
|
|
448
|
+
|
|
449
|
+
### Grab-to-scroll horizontal
|
|
450
|
+
|
|
451
|
+
Arrastar o corpo da tabela (mouse/pen) pra rolar lateralmente — equivalente ao `useGrabToScroll` legado.
|
|
452
|
+
|
|
453
|
+
```tsx
|
|
454
|
+
{/* nativo — nada a fazer; pra desligar: */}
|
|
455
|
+
<DataTable<Client> rows={clients} columns={columns} grabToScroll={false} />
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
- Prop raiz `grabToScroll: boolean` — **nativo, default `true`** (todas as tabelas já vêm com ele; passe `false` pra desabilitar).
|
|
459
|
+
- Um arrasto só inicia após ~6px de movimento → clique/seleção de célula preservados; o clique pós-arrasto é suprimido.
|
|
460
|
+
- **Scroll por roda do mouse permanece intacto.** Pulado em touch (scroll nativo já funciona) e em alvos interativos (botões, inputs, células editáveis/expansíveis/de seleção/ações).
|
|
461
|
+
- **O cursor `grab` só aparece quando há overflow** — tabela que cabe na tela não mostra a mãozinha, e volta a mostrar se uma coluna crescer, a view trocar ou o container encolher (re-medido por `ResizeObserver`). Até 2026-08-21 a mãozinha era incondicional e prometia um gesto que o handler recusava: se você viu isso num projeto, é versão anterior.
|
|
462
|
+
|
|
463
|
+
### Tela cheia (fullscreen)
|
|
464
|
+
|
|
465
|
+
Toggle ⤢ na toolbar expande a DataTable pra ocupar a viewport inteira; segundo clique ou **Esc** volta.
|
|
466
|
+
|
|
467
|
+
```tsx
|
|
468
|
+
<DataTable<Client>
|
|
469
|
+
rows={clients}
|
|
470
|
+
columns={columns}
|
|
471
|
+
toolbar={{ enableFullscreen: true }}
|
|
472
|
+
/>
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
- `toolbar.enableFullscreen: true` (default `false`) renderiza o tool button entre Filtros e Configurações.
|
|
476
|
+
- O container raiz vira overlay `fixed inset-0` (z-index `--z-index-modal`) com bg do canvas. Estado interno uncontrolled.
|
|
477
|
+
|
|
478
|
+
### View Kanban (table ⇄ board)
|
|
479
|
+
|
|
480
|
+
```tsx
|
|
481
|
+
<DataTable<Client>
|
|
482
|
+
rows={clients}
|
|
483
|
+
columns={columns}
|
|
484
|
+
defaultViewMode="kanban" // uncontrolled — ou viewMode + onViewModeChange (controlled)
|
|
485
|
+
kanbanConfig={{
|
|
486
|
+
groupByField: "status", // valor do field define a coluna do board
|
|
487
|
+
renderCard: ({ row }) => ({
|
|
488
|
+
// slots do card — id/columnId são derivados automaticamente
|
|
489
|
+
title: row.name,
|
|
490
|
+
subtitle: row.email,
|
|
491
|
+
value: formatBRL(row.value),
|
|
492
|
+
}),
|
|
493
|
+
enableDnD: true,
|
|
494
|
+
onCardMove: (cardId, from, to) => patchStatus(cardId, to),
|
|
495
|
+
}}
|
|
496
|
+
/>
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
Quando `viewMode`/`defaultViewMode` + `kanbanConfig` estão definidos, a toolbar auto-renderiza o segmented table/kanban (override/esconda via `toolbar.viewToggle`). Filter/search/sort/selection continuam aplicados às rows; paginação, density toggle e columns popover são desligados automaticamente no board. `kanbanConfig.columns` (opcional, `KanbanColumn[]`) fixa ordem/label/dotColor das colunas — sem ele, as colunas derivam dos valores únicos de `groupByField`. Outras opções do `kanbanConfig`: `renderCardContent` (override total do miolo do card), `getCardMenuItems`/`getColumnMenuItems` (menus "⋯"), `onAddCard`/`onAddInFooter`, `emptyLabel`/`addLabel`.
|
|
500
|
+
|
|
501
|
+
### View Lista (table ⇄ list)
|
|
502
|
+
|
|
503
|
+
```tsx
|
|
504
|
+
<DataTable<Client>
|
|
505
|
+
rows={clients}
|
|
506
|
+
columns={columns}
|
|
507
|
+
viewMode={viewMode} // "table" | "list" | "kanban"
|
|
508
|
+
onViewModeChange={setViewMode} // ou defaultViewMode (uncontrolled)
|
|
509
|
+
listConfig={{
|
|
510
|
+
renderItem: (row, { depth }) => (
|
|
511
|
+
<div className="flex w-full items-center gap-gp-lg">
|
|
512
|
+
<Avatar size="md" colorHex={row.avatarColor}>{row.initials}</Avatar>
|
|
513
|
+
<div className="flex min-w-0 flex-1 flex-col">
|
|
514
|
+
<span className="truncate text-body-md font-semibold">{row.name}</span>
|
|
515
|
+
<span className="truncate text-caption-md text-fg-muted">{row.email}</span>
|
|
516
|
+
</div>
|
|
517
|
+
<Chip color="success" variant="soft" size="sm" shape="pill">Ativo</Chip>
|
|
518
|
+
</div>
|
|
519
|
+
),
|
|
520
|
+
// hierarchical: true, // + getTreeDataPath → lista em ÁRVORE (conectores)
|
|
521
|
+
// getMenuItems: (row) => [...], // menu "⋯" por item
|
|
522
|
+
}}
|
|
523
|
+
/>
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
`listConfig` habilita a 3ª view: o toggle vira **Tabela / Lista** (+ Kanban se `kanbanConfig`). O DataTable mantém a **MESMA toolbar** (busca/filtros/views/ações/totalizadores) e só troca o corpo por um `<List>` do DS, alimentado pelas rows processadas (`filter+search+sort`; por padrão **sem paginação** — mostra todas, igual ao kanban). Passe `listConfig.paginated: true` pra **paginar a lista flat** (usa a mesma paginação da tabela + mostra o footer; ignorado em `hierarchical`). `listConfig.renderItem(row, { depth, open })` desenha o card de cada item. Com `hierarchical: true`, a lista aninha em árvore com indentação/conectores e `depth` por nível, usando `listConfig.getPath` (caminho raiz→self) — ou, se ausente, o `getTreeDataPath` do DataTable. **Use `listConfig.getPath` quando quiser tabela FLAT (paginada) + lista em ÁRVORE** no mesmo DataTable (o `getTreeDataPath` ligaria o tree-data na tabela e desligaria a paginação). Showcase: `#/clients-list-view`.
|
|
527
|
+
|
|
528
|
+
### Saved views
|
|
529
|
+
|
|
530
|
+
```tsx
|
|
531
|
+
import { savedViewsMockService } from "@/components/ui/DataTable";
|
|
532
|
+
|
|
533
|
+
<DataTable<Client>
|
|
534
|
+
rows={clients}
|
|
535
|
+
columns={columns}
|
|
536
|
+
savedViewsService={savedViewsMockService} // troque pelo seu service em prod
|
|
537
|
+
/>;
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
Service contract em `services/saved-views.types.ts` — `list / save / delete` (todos recebem `persistId` como primeiro arg). Persiste o `DataTableSavedViewState` (filterModel, sortModel, density, layout de colunas, viewMode, groupBy, expandedRowIds) como JSON — `search` e paginação são voláteis e NÃO entram na view.
|
|
541
|
+
|
|
542
|
+
**`allowCreateView` (v0.23.0)** — `allowCreateView={false}` esconde o botão "+" das visões (exibe SÓ os `defaultViews` + Default, read-only; o usuário não cria/salva visões). Default `true`.
|
|
543
|
+
|
|
544
|
+
**`maxViewTabs`** — quantas abas de visão cabem na barra, **contando a "Default"**. Default `3`, ou seja **2 presets** de `defaultViews` viram aba.
|
|
545
|
+
|
|
546
|
+
⚠️ O excedente é **cortado** (`.slice()` no `TableToolbarViews`): com 3 presets, o terceiro não aparece — sem erro e sem overflow. Em **DEV** sai um `console.warn` (v0.43.1) nomeando o que foi cortado e o `maxViewTabs` que resolve; em produção o corte é silencioso. Precisa de N abas fixas? `maxViewTabs={N + 1}`. Lembre que a aba **Default já é a visão sem filtro** — preset "Todos"/"Todas" duplica ela e gasta um slot.
|
|
547
|
+
|
|
548
|
+
**viewMode "sticky" ao trocar de visão (v0.23.0)** — aplicar uma visão (preset/Default) só troca o `viewMode` se a visão **definir um explicitamente** (ex.: preset salvo em Lista/Kanban). Presets sem `viewMode` (o caso comum) **mantêm** o que o usuário está vendo — alternar de visão não flipa Tabela↔Lista↔Kanban. Pra um preset abrir numa view específica, passe `viewMode` no `presetView({ ... })`.
|
|
549
|
+
|
|
550
|
+
### Tipo de coluna custom (registry)
|
|
551
|
+
|
|
552
|
+
```tsx
|
|
553
|
+
const RatingColumnType: ColumnTypeDefinition = {
|
|
554
|
+
type: "rating",
|
|
555
|
+
// operators: array de { id, label } — id usa nomes canônicos do FilterOperator
|
|
556
|
+
// (equals, neq, contains, notContains, startsWith, endsWith, gt, lt, gte, lte,
|
|
557
|
+
// isAnyOf, isNoneOf, between, isEmpty, isNotEmpty). Label aparece no dropdown
|
|
558
|
+
// de operadores do popover Filtros + chip toolbar (via DEFAULT_OP_LABELS).
|
|
559
|
+
operators: [
|
|
560
|
+
{ id: "equals", label: "é" },
|
|
561
|
+
{ id: "gt", label: "maior que" },
|
|
562
|
+
{ id: "lt", label: "menor que" },
|
|
563
|
+
],
|
|
564
|
+
renderCell: ({ value }) => <Stars n={Number(value) || 0} />,
|
|
565
|
+
renderFilterInput: ({ value, onChange }) =>
|
|
566
|
+
<NumberInput min={0} max={5} value={value} onChange={onChange} />,
|
|
567
|
+
// obrigatório (sem `?` no type) — input do popover do chip rápido.
|
|
568
|
+
// Pode reusar o mesmo widget do renderFilterInput.
|
|
569
|
+
renderFastFilterInput: ({ value, onChange }) =>
|
|
570
|
+
<NumberInput min={0} max={5} value={value} onChange={onChange} />,
|
|
571
|
+
matchesFilter: (cellValue, filterValue, operator) => {
|
|
572
|
+
const n = Number(cellValue) || 0;
|
|
573
|
+
const f = Number(filterValue) || 0;
|
|
574
|
+
if (operator === "equals") return n === f;
|
|
575
|
+
if (operator === "gt") return n > f;
|
|
576
|
+
if (operator === "lt") return n < f;
|
|
577
|
+
return null;
|
|
578
|
+
},
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
// Registro feito uma vez na boot:
|
|
582
|
+
columnTypeRegistry.register(RatingColumnType);
|
|
583
|
+
|
|
584
|
+
// Uso:
|
|
585
|
+
{ field: "rating", headerName: "Rating", type: "rating" as any }
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
---
|
|
589
|
+
|
|
590
|
+
## Filtros — funil (drawer simple) + avançado (Configurações)
|
|
591
|
+
|
|
592
|
+
A toolbar separa dois níveis de filtro, sempre disponíveis (gated por
|
|
593
|
+
`toolbar.enableFilters !== false` + colunas filtráveis):
|
|
594
|
+
|
|
595
|
+
- **Funil** → abre um **drawer lateral** com TODOS os filtros em form vertical.
|
|
596
|
+
Aplicação LIVE, operator inferido do `filterType` (multiSelect → isAnyOf,
|
|
597
|
+
text → contains, date → between, etc). O caminho simples pro user típico.
|
|
598
|
+
- **Configurações → Filtros avançados** → query builder completo: modo **Visual**
|
|
599
|
+
(AND/OR + operadores explícitos + Adicionar condição) e modo **Avançado** (SQL-like,
|
|
600
|
+
round-trip-safe pra todos os operadores).
|
|
601
|
+
|
|
602
|
+
A prop `simpleFilter` é **opcional** — só customiza o drawer do funil:
|
|
603
|
+
|
|
604
|
+
```tsx
|
|
605
|
+
{/* Funil + avançado vêm de graça — sem configuração */}
|
|
606
|
+
<DataTable rows={...} columns={...} />
|
|
607
|
+
|
|
608
|
+
{/* Customizar o drawer do funil */}
|
|
609
|
+
<DataTable
|
|
610
|
+
simpleFilter={{
|
|
611
|
+
hiddenFields: ["internal"], // não mostra no drawer (só no avançado)
|
|
612
|
+
title: "Refinar busca",
|
|
613
|
+
size: "lg", // 560px (default md = 400px)
|
|
614
|
+
}}
|
|
615
|
+
/>
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
### `simpleFilter` (opcional)
|
|
619
|
+
|
|
620
|
+
| Prop | Tipo | Default | Quando usar |
|
|
621
|
+
| --------------------------- | ------------------------------ | -------------- | -------------------------------------------------------------------------------------- |
|
|
622
|
+
| `simpleFilter.hiddenFields` | `string[]` | `[]` | Fields que NÃO aparecem no drawer do funil (só no avançado). Útil pra filtros técnicos |
|
|
623
|
+
| `simpleFilter.title` | `string` | `"Filtros"` | Override do título do header do drawer |
|
|
624
|
+
| `simpleFilter.size` | `"sm" \| "md" \| "lg" \| "xl"` | `"md"` (400px) | Use `"lg"` (560px) se há muitos campos com widgets largos (dates/multi-select) |
|
|
625
|
+
|
|
626
|
+
---
|
|
627
|
+
|
|
628
|
+
## ⚠️ filterModel controlado — operator correto por filterType
|
|
629
|
+
|
|
630
|
+
**Quando passar `filterModel` como prop controlada (em vez de uncontrolled), use o operator
|
|
631
|
+
correto pro `filterType` da coluna.** Operator errado = popover Filtros mostra o Select de
|
|
632
|
+
operador **vazio** porque o operator não está nos `operators` do column-type.
|
|
633
|
+
|
|
634
|
+
| `filterType` da coluna | Operators válidos | Default sugerido |
|
|
635
|
+
| ---------------------- | --------------------------------------------------------------------------------------------- | ---------------- |
|
|
636
|
+
| `multiSelect` | `isAnyOf`, `isNoneOf`, `isEmpty`, `isNotEmpty` | `isAnyOf` |
|
|
637
|
+
| `select` | `equals`, `neq`, `isEmpty`, `isNotEmpty` | `equals` |
|
|
638
|
+
| `text` (default) | `contains`, `notContains`, `equals`, `neq`, `startsWith`, `endsWith`, `isEmpty`, `isNotEmpty` | `contains` |
|
|
639
|
+
| `number` | `equals`, `neq`, `gt`, `lt`, `gte`, `lte` | `equals` |
|
|
640
|
+
| `date` | `between`, `equals`, `gt`, `lt`, `gte`, `lte` | `between` |
|
|
641
|
+
| `boolean` | `equals` | `equals` |
|
|
642
|
+
|
|
643
|
+
```tsx
|
|
644
|
+
// ❌ ERRADO — Status é multiSelect mas operator é "equals"
|
|
645
|
+
// → Popover Filtros mostra Select operador VAZIO
|
|
646
|
+
const INITIAL_FILTERS: FilterModel = {
|
|
647
|
+
items: [{ id: "f1", field: "statusId", operator: "equals", value: "active" }],
|
|
648
|
+
logicOperator: "AND",
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
// ✅ CORRETO — operator bate com filterType=multiSelect
|
|
652
|
+
const INITIAL_FILTERS: FilterModel = {
|
|
653
|
+
items: [
|
|
654
|
+
{ id: "f1", field: "statusId", operator: "isAnyOf", value: "active" },
|
|
655
|
+
],
|
|
656
|
+
logicOperator: "AND",
|
|
657
|
+
};
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
**Defesa em profundidade:** `FilterRowEditor` detecta operator inválido e faz fallback pro
|
|
661
|
+
primeiro operator do column-type + auto-normaliza via `onChange`. Mas é melhor declarar
|
|
662
|
+
correto desde o início.
|
|
663
|
+
|
|
664
|
+
**Atalho pra presets uncontrolled:** se você usar `defaultViews={[presetView({...})]}` em
|
|
665
|
+
vez de filterModel controlled, o controller normaliza automaticamente via
|
|
666
|
+
`normalizeFilterModelForColumns` na hidratação. Só recomendado se você não precisa de
|
|
667
|
+
controle externo do filterModel.
|
|
668
|
+
|
|
669
|
+
---
|
|
670
|
+
|
|
671
|
+
## Imperative ref
|
|
672
|
+
|
|
673
|
+
```tsx
|
|
674
|
+
const tableRef = useRef<DataTableRef>(null);
|
|
675
|
+
|
|
676
|
+
<DataTable ref={tableRef} ... />
|
|
677
|
+
|
|
678
|
+
tableRef.current?.getSelectedIds(); // (string | number)[]
|
|
679
|
+
tableRef.current?.getSelectedCount(); // number
|
|
680
|
+
tableRef.current?.clearSelection();
|
|
681
|
+
tableRef.current?.getState(); // DataTableState snapshot
|
|
682
|
+
tableRef.current?.refresh(); // server mode: re-disparar fetchData
|
|
683
|
+
tableRef.current?.exportCsv("filtered"); // download CSV — escopo "all" | "filtered" | "selected"
|
|
684
|
+
tableRef.current?.resetPersistedState(); // limpa o localStorage (no-op sem persistId)
|
|
685
|
+
tableRef.current?.expandAllTree(); // tree-data: expande todos os nós (no-op fora de tree-data)
|
|
686
|
+
tableRef.current?.collapseAllTree(); // tree-data: recolhe todos os nós (no-op fora de tree-data)
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
`expandAllTree` / `collapseAllTree` só fazem efeito em modo tree-data (`getTreeDataPath`). Operam sobre todas as rows pós-filtro/sort (tree-data desliga paginação) via `collectExpandableTreeIds` e respeitam `treeData.defaultExpanded` — escrevem o Set de divergência correto (`[]` ou todos os ids expansíveis).
|
|
690
|
+
|
|
691
|
+
---
|
|
692
|
+
|
|
693
|
+
## Configs detalhadas
|
|
694
|
+
|
|
695
|
+
### `toolbar` (DataTableToolbarConfig)
|
|
696
|
+
|
|
697
|
+
- `title?` — string no canto esquerdo
|
|
698
|
+
- `enableSearch?` (true) — ToolbarSearch slot
|
|
699
|
+
- `enableRefresh?` (true) — botão Refresh após o search (server mode refetch; client mode spinner)
|
|
700
|
+
- `enableFilters?` (true) — controle de filtros (só aparece se ao menos uma coluna tem `enableColumnFilter`)
|
|
701
|
+
- `enableColumns?` (true) — ColsPopover (show/hide, pin, reorder via drag)
|
|
702
|
+
- `enableDensity?` (true) — ToolbarSegmented compact/standard/comfortable
|
|
703
|
+
- `enableExport?` (false) — `true` = dropdown Exportar com CSV default; objeto `{ formats?, items? }` pra formatos custom
|
|
704
|
+
- `enableFullscreen?` (false) — botão ⤢ na toolbar (entre Filtros e Configurações) que expande a tabela pra viewport inteira; Esc volta
|
|
705
|
+
- `moreMenu?` — `{ items: DataTableMoreMenuItem[] }` — MoreMenu (⋯) no canto direito
|
|
706
|
+
- `customLeft?` — ReactNode livre após search/refresh (controls custom)
|
|
707
|
+
- `viewToggle?` — override/esconde o segmented table/kanban auto-renderizado
|
|
708
|
+
|
|
709
|
+
> Bulk actions vão em `selectionConfig.actions` (não no toolbar). Preset views vão na prop
|
|
710
|
+
> `defaultViews` da DataTable (não no toolbar).
|
|
711
|
+
|
|
712
|
+
### `paginationConfig`
|
|
713
|
+
|
|
714
|
+
- `enabled` (true)
|
|
715
|
+
- `initialPageSize` (25)
|
|
716
|
+
- `pageSizeOptions` ([10, 25, 50, 100])
|
|
717
|
+
|
|
718
|
+
> O modo client/server **não é prop** — é derivado automaticamente de `rows` vs `fetchData`.
|
|
719
|
+
|
|
720
|
+
### `selectionConfig`
|
|
721
|
+
|
|
722
|
+
- `enabled` (false)
|
|
723
|
+
- `enableGlobal` (false) — "selecionar todos" com modo include/exclude
|
|
724
|
+
- `actions?: (selectedIds: GridRowId[], clearSelection: () => void) => ReactNode` — render-prop chamada dentro do BulkActionsBar (NÃO é ReactNode direto)
|
|
725
|
+
|
|
726
|
+
### `getRowId?: (row: T) => GridRowId` (prop raiz)
|
|
727
|
+
|
|
728
|
+
Extrai o id da row — default `row.id`. É prop top-level da DataTable, **não** vai dentro de `selectionConfig`.
|
|
729
|
+
|
|
730
|
+
### `densityItems?: ToolbarSegmentedItem<TableDensity>[]`
|
|
731
|
+
|
|
732
|
+
Customiza os 3 botões do segmented. Default: compact / standard / comfortable.
|
|
733
|
+
|
|
734
|
+
### `cardBreakpoint?: number | false`
|
|
735
|
+
|
|
736
|
+
- `number` (default `768`) — viewport `< N px` ativa o card mode (rows viram `<TableCardRow>`)
|
|
737
|
+
- `false` — desabilita o auto-switch (mantém table view em qualquer viewport)
|
|
738
|
+
|
|
739
|
+
Use `false` em telas onde o card mode não faz sentido (ex: tabela dentro de modal pequeno que já é mobile-friendly de outra forma).
|
|
740
|
+
|
|
741
|
+
### `autoFit?: boolean` (default `true`)
|
|
742
|
+
|
|
743
|
+
Auto-distribui as colunas para ocupar todo o container, em 3 camadas:
|
|
744
|
+
|
|
745
|
+
1. **Type Heuristics** — cada `column.type` tem `defaultWidth` do registry. Se a coluna define `width`, esse vira a **base/mínimo** da coluna (ver Flex Distribution).
|
|
746
|
+
2. **Smart Content Sampling** — mede o texto do header + primeiras 20 rows via canvas (`measureText`) e ajusta width pra caber o conteúdo. Respeita `col.minWidth` e `col.maxWidth`.
|
|
747
|
+
3. **Flex Distribution (proporcional)** — sobrando espaço no container, distribui **proporcionalmente** entre as colunas (peso = largura-base de cada uma), como uma tabela flex faz naturalmente. Colunas pequenas crescem pouco, largas crescem mais — sem "coluna gigante" puxando 100% do espaço. Funciona pra qualquer nº de colunas.
|
|
748
|
+
|
|
749
|
+
**Header nunca trunca (`...`):** toda coluna tem como piso a largura necessária pra mostrar o `headerName` inteiro (texto + ícone de tipo + reserva de sort/menu). Isso vale **inclusive** pra colunas com `width` explícito menor que o header — a width do consumer não pode esconder o título (só `maxWidth` menor que o header trunca, e aí é decisão explícita do consumer).
|
|
750
|
+
|
|
751
|
+
**`col.width` é base, não trava fixa (v0.22.0+):** colunas com `width` explícito entram na distribuição proporcional usando a width como piso (crescem pra preencher, nunca encolhem abaixo dela). Antes a width era 100% fixa, o que jogava todo o espaço sobrando na única coluna sem width (virava "coluna gigante"). Pra travar uma coluna de fato, use `width` + `maxWidth` iguais (ou um `type` fixo como `actions`/`checkbox`, que ficam fora do flex). Se **todas** as colunas têm `width` explícito, o layout fixo do consumer é respeitado e o espaço sobrando fica vazio à direita.
|
|
752
|
+
|
|
753
|
+
Observado via `ResizeObserver` no container — recalcula quando viewport muda. Re-mede e re-aplica de forma consistente ao alternar **Tabela ↔ Lista** (o corpo da tabela desmonta na view Lista; ao voltar, o autoFit reata o observer no node novo — mesma distribuição da 1ª carga).
|
|
754
|
+
|
|
755
|
+
**Precedência de width:** resize manual (drag pelo user) > autoFit > `col.width` > `typeDef.defaultWidth`.
|
|
756
|
+
|
|
757
|
+
**Para desligar:** `autoFit={false}` mantém comportamento legacy (cada coluna usa `col.width` ou default fixo; espaço sobrando vira vazio à direita). Resize manual continua disponível em ambos os modos.
|
|
758
|
+
|
|
759
|
+
```tsx
|
|
760
|
+
// Default — fluid automático
|
|
761
|
+
<DataTable rows={rows} columns={cols} />
|
|
762
|
+
|
|
763
|
+
// Opt-out
|
|
764
|
+
<DataTable rows={rows} columns={cols} autoFit={false} />
|
|
765
|
+
|
|
766
|
+
// width = BASE/mínimo (cresce proporcional p/ preencher). Pra TRAVAR de fato,
|
|
767
|
+
// use width + maxWidth iguais, ou um type fixo (actions/checkbox).
|
|
768
|
+
const cols = [
|
|
769
|
+
{ field: "id", width: 80, maxWidth: 80 }, // travada em 80px
|
|
770
|
+
{ field: "code", width: 120 }, // base 120, cresce no flex
|
|
771
|
+
{ field: "name" }, // sem width — flui pelo autoFit
|
|
772
|
+
{ field: "actions", type: "actions", getActions }, // fora do flex; largura pelo nº de ações
|
|
773
|
+
];
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
> Nota: o melhor padrão é **não** setar `width` nas colunas de dados e deixar o autoFit
|
|
777
|
+
> distribuir (as skills `crud-builder`/`list-builder` geram assim). Setar `width` em
|
|
778
|
+
> todas as colunas trava o layout e deixa espaço vazio à direita.
|
|
779
|
+
|
|
780
|
+
### `grabToScroll?: boolean` (**nativo — default `true`**)
|
|
781
|
+
|
|
782
|
+
Grab-to-scroll horizontal: arrastar o corpo da tabela (mouse/pen) rola lateralmente. **Já vem ligado em todas as tabelas** — não precisa configurar. Threshold de ~6px separa arrasto de clique (seleção/click de célula preservados; o clique pós-arrasto é suprimido). Scroll por roda intacto; pulado em touch e alvos interativos. Passe `grabToScroll={false}` só se quiser desabilitar.
|
|
783
|
+
|
|
784
|
+
```tsx
|
|
785
|
+
{/* nativo — nada a fazer. Pra desligar: */}
|
|
786
|
+
<DataTable rows={rows} columns={cols} grabToScroll={false} />
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
### `persistId?: string` (workspace "Default" persistente — schema v4)
|
|
790
|
+
|
|
791
|
+
Quando definido, **todo** o workspace "Default" é salvo em localStorage:
|
|
792
|
+
|
|
793
|
+
- `density`, `sortModel`, `pageSize`, `currentPage`
|
|
794
|
+
- `columnWidths` (resize manual), `pinnedColumns`, `hiddenColumns`, `columnOrder`
|
|
795
|
+
- `filterModel`, `search` (texto debounced)
|
|
796
|
+
- `viewMode`, `groupBy`, `expandedRowIds`
|
|
797
|
+
- `lastActiveViewId` — qual view estava aplicada no último uso
|
|
798
|
+
|
|
799
|
+
**Como views custom interagem com Default:**
|
|
800
|
+
|
|
801
|
+
- User filtra/busca/etc → snapshot da Default é atualizado em tempo real
|
|
802
|
+
- User aplica view custom (preset ou saved) → snapshot da Default fica **congelado** (não polui)
|
|
803
|
+
- User volta para Default → `applyDefault` restaura tudo (filter, search, page, etc) do snapshot intacto
|
|
804
|
+
- User precisa **limpar manualmente** (clear search input, remover filtros via UI) para resetar
|
|
805
|
+
|
|
806
|
+
**Reset programático:**
|
|
807
|
+
|
|
808
|
+
```ts
|
|
809
|
+
ref.current?.resetPersistedState(); // remove entry inteira do localStorage
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
**Schema versionado:** entries antigos (v3 ou menor) são descartados silenciosamente — DataTable cai no comportamento default sem erro. Schema atual `v4`.
|
|
813
|
+
|
|
814
|
+
---
|
|
815
|
+
|
|
816
|
+
## Performance
|
|
817
|
+
|
|
818
|
+
- `columns` **deve** ser memoizado com `useMemo` no pai
|
|
819
|
+
- O processor (filter → search → sort → paginate) usa useMemo cascateado — mudar só de página NÃO re-roda filter/search/sort
|
|
820
|
+
- Provider value é memoizado — re-render do pai não dispara cascade em rows
|
|
821
|
+
- Use `virtualize` para > ~500 rows visíveis (ou desde sempre se UX permite)
|
|
822
|
+
- Saved views + persistId: ambos consomem o mesmo `DataTableState`, podem coexistir
|
|
823
|
+
|
|
824
|
+
---
|
|
825
|
+
|
|
826
|
+
## ARIA
|
|
827
|
+
|
|
828
|
+
Tudo proveniente do `<Table>` primitive: `role="grid"`, `role="row"`, `role="columnheader"` com `aria-sort`, `role="gridcell"`. Keyboard navigation segue WAI-ARIA grid pattern. Bulk bar tem `role="region"` com aria-label.
|
|
829
|
+
|
|
830
|
+
---
|
|
831
|
+
|
|
832
|
+
## Troubleshooting
|
|
833
|
+
|
|
834
|
+
| Sintoma | Causa provável | Fix |
|
|
835
|
+
| ---------------------------------------- | -------------------------------------------- | -------------------------------------------------------------------------------- |
|
|
836
|
+
| Tabela re-renderiza inteira a cada digit | `columns` não memoizado | `useMemo(() => [...], [deps])` |
|
|
837
|
+
| Filter chip não aparece | Coluna sem `enableColumnFilter: true` | Adicionar flag |
|
|
838
|
+
| Filter popover vazio | Nenhuma coluna com `enableColumnFilter` | Adicionar a pelo menos 1 coluna |
|
|
839
|
+
| Sort não funciona | Coluna sem `sortable: true` | Adicionar flag |
|
|
840
|
+
| Density não persiste | `persistId` ausente | Adicionar `persistId="meu-table"` |
|
|
841
|
+
| Server mode loop infinito | `fetchData` não memoizado | `useCallback(fetchData, [deps])` |
|
|
842
|
+
| Virtualização "pula" | `estimateRowHeight` muito diferente do real | Ajustar pro height médio observado |
|
|
843
|
+
| Inline edit não salva | `onCellEditCommit` retorna sem await | Retornar Promise; controller aguarda |
|
|
844
|
+
| Saved views não persiste | `savedViewsMockService` em prod | Implementar `SavedViewsService` real |
|
|
845
|
+
| Group header sem totalizer | Coluna sem `aggregate` declarado | Definir `aggregate: "sum" \| "avg" \| "count" \| "min" \| "max" \| fn` na coluna |
|
|
846
|
+
| Coluna actions com filter chip | `type: "actions"` deveria desabilitar filter | Reportar — esse type bloqueia sort/filter por design |
|
|
847
|
+
|
|
848
|
+
---
|
|
849
|
+
|
|
850
|
+
## Padrões internos (referência rápida)
|
|
851
|
+
|
|
852
|
+
- **God component evitado** — DataTable orquestra; lógica pesada em hooks (`use-filter-popover-adapter`, `use-sort-popover-adapter`, `use-cols-popover-adapter`, `use-data-table-processor`, etc)
|
|
853
|
+
- **Vocabulário único de operador** — ids longos do `FilterModel` (`equals`, `neq`, `gt`, `gte`…) em todo o fluxo (popover, parser SQL, chips, adapter). Sem tradução curto↔longo. Label do chip vem do registry do column-type (`opLabel`), com `DEFAULT_OP_LABELS` como fallback
|
|
854
|
+
- **Value resolution shared** — `utils/resolve-value.ts` (`getFieldValue / applyValueGetter / applyFormatter`) usado por processor, group-rows e cell render
|
|
855
|
+
- **Column types via registry** — `column-types/column-type-registry.ts`; `console.warn` em duplicate (não throw, suporta hot reload)
|
|
856
|
+
- **Row variants discriminadas** — `groupRow / groupContentRow / expansionRow / dataRow` via Symbol-as-discriminator (type-safe)
|
|
857
|
+
- **Sortable head cell renomeado** — `DataTableSortableHeadCell` (consistência com prefixo)
|
|
858
|
+
|
|
859
|
+
---
|
|
860
|
+
|
|
861
|
+
## V2 (planejado, não V1)
|
|
862
|
+
|
|
863
|
+
- Extração final do `<DataTableBody>` para componente próprio
|
|
864
|
+
- Hooks dedicados pra inline-edit, keyboard-nav, grouping, expansion (atualmente inline no orquestrador)
|
|
865
|
+
- Migration helper auto-aplicado para saved views quando colunas removidas
|
|
866
|
+
- Unit tests cobertura ≥ 80% nos hooks críticos (`use-column-resize`, `group-rows`, `expand-rows`, `use-data-table-processor`)
|
|
867
|
+
- Mobile parts da toolbar (`ToolbarMobileDialog` etc) decidir mantém ou remove
|