@aircall/blocks 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -0
- package/dist/globals.css +1 -1
- package/dist/index.d.ts +80 -23
- package/dist/index.js +339 -86
- package/package.json +17 -3
- package/skills/aircall-blocks/migrate-dashboard/SKILL.md +127 -0
- package/skills/aircall-blocks/migrate-dashboard/card/SKILL.md +364 -0
- package/skills/aircall-blocks/migrate-dashboard/combobox/SKILL.md +487 -0
- package/skills/aircall-blocks/migrate-dashboard/copy-button/SKILL.md +138 -0
- package/skills/aircall-blocks/migrate-dashboard/dashboard-page/SKILL.md +198 -0
- package/skills/aircall-blocks/migrate-dashboard/data-table/SKILL.md +395 -0
- package/skills/aircall-blocks/migrate-dashboard/empty-states/SKILL.md +347 -0
- package/skills/aircall-blocks/migrate-dashboard/form-wizard/SKILL.md +271 -0
- package/skills/aircall-blocks/migrate-dashboard/info-popup/SKILL.md +179 -0
- package/skills/aircall-blocks/migrate-dashboard/layout/SKILL.md +328 -0
- package/skills/aircall-blocks/migrate-dashboard/list/SKILL.md +474 -0
- package/skills/aircall-blocks/migrate-dashboard/loading/SKILL.md +260 -0
- package/skills/aircall-blocks/migrate-dashboard/page-header/SKILL.md +394 -0
- package/skills/aircall-blocks/migrate-dashboard/tile/SKILL.md +429 -0
- package/skills/aircall-blocks/migrate-dashboard/toggle-row/SKILL.md +177 -0
- package/skills/aircall-blocks/setup/SKILL.md +127 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: aircall-blocks/migrate-dashboard/dashboard-page
|
|
3
|
+
description: >
|
|
4
|
+
Migrate a screen's page shell to @aircall/blocks — DashboardPage (standard in-app page:
|
|
5
|
+
sidebar + header + optional tabs + content) vs DashboardStandalonePage (full-page focused
|
|
6
|
+
flow with no sidebar, e.g. Campaign Creation, Add Contacts). Load when migrating a screen's
|
|
7
|
+
outer layout, a full-page wizard/creation flow, or a page header+content scaffold.
|
|
8
|
+
type: sub-skill
|
|
9
|
+
library: aircall-blocks
|
|
10
|
+
library_version: "0.5.1"
|
|
11
|
+
requires:
|
|
12
|
+
- aircall-blocks/setup
|
|
13
|
+
- aircall-blocks/migrate-dashboard
|
|
14
|
+
sources:
|
|
15
|
+
- "aircall/hydra:packages/blocks/src/components/dashboard-page.tsx"
|
|
16
|
+
- "aircall/hydra:packages/blocks/src/components/dashboard-standalone-page.tsx"
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
This skill builds on aircall-blocks/migrate-dashboard.
|
|
20
|
+
|
|
21
|
+
## 1. Which shell?
|
|
22
|
+
|
|
23
|
+
| Screen kind | Use | Why |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| Standard in-app page (sidebar + header + optional tabs + content) | `DashboardPage` | Lives inside the dashboard chrome |
|
|
26
|
+
| Full-page focused flow — creation wizard, Add Contacts, standalone forms (no sidebar) | `DashboardStandalonePage` | Takes over the viewport; header is just title + actions |
|
|
27
|
+
|
|
28
|
+
Campaign Creation (`CampaignWizardPage`) and Add Contacts (`AddContactsPage`) are **standalone** flows → `DashboardStandalonePage`, with the multi-step form inside (see `@aircall/blocks#aircall-blocks/migrate-dashboard/form-wizard`).
|
|
29
|
+
|
|
30
|
+
## 2. DashboardStandalonePage (full-page flows)
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import {
|
|
34
|
+
DashboardStandalonePage,
|
|
35
|
+
DashboardStandalonePageHeader,
|
|
36
|
+
DashboardStandalonePageTitle,
|
|
37
|
+
DashboardStandalonePageActions,
|
|
38
|
+
DashboardStandalonePageAction,
|
|
39
|
+
DashboardStandalonePageContent
|
|
40
|
+
} from '@aircall/blocks';
|
|
41
|
+
|
|
42
|
+
<DashboardStandalonePage>
|
|
43
|
+
<DashboardStandalonePageHeader>
|
|
44
|
+
<DashboardStandalonePageTitle>Create campaign</DashboardStandalonePageTitle>
|
|
45
|
+
<DashboardStandalonePageActions>
|
|
46
|
+
<DashboardStandalonePageAction onClick={onCancel}>Cancel</DashboardStandalonePageAction>
|
|
47
|
+
</DashboardStandalonePageActions>
|
|
48
|
+
</DashboardStandalonePageHeader>
|
|
49
|
+
<DashboardStandalonePageContent>
|
|
50
|
+
{/* a centered, width-capped Card — see below */}
|
|
51
|
+
</DashboardStandalonePageContent>
|
|
52
|
+
</DashboardStandalonePage>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### What goes inside `DashboardStandalonePageContent` — a centered, width-capped Card
|
|
56
|
+
|
|
57
|
+
`DashboardStandalonePageContent` is a full-bleed scroll area; it does **not** constrain
|
|
58
|
+
width. The actual content is a centered container (`mx-auto` + a `max-w-*` cap) wrapping
|
|
59
|
+
one or more `Card`s — the consistent pattern across the `DashboardStandalonePage` stories.
|
|
60
|
+
Don't let content run full-width, and don't reach for `Paper`/raw page wrappers.
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
import { Card, CardHeader, CardContent, CardFooter } from '@aircall/ds';
|
|
64
|
+
|
|
65
|
+
<DashboardStandalonePageContent>
|
|
66
|
+
{/* single-column form / wizard */}
|
|
67
|
+
<div className="mx-auto max-w-2xl pt-4">
|
|
68
|
+
<Card>
|
|
69
|
+
<CardHeader>{/* title, or Stepper progress for a wizard */}</CardHeader>
|
|
70
|
+
<CardContent>{/* fields */}</CardContent>
|
|
71
|
+
<CardFooter className="justify-end gap-2">{/* Back / Next / Submit */}</CardFooter>
|
|
72
|
+
</Card>
|
|
73
|
+
</div>
|
|
74
|
+
</DashboardStandalonePageContent>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Width cap by layout:
|
|
78
|
+
- **Single-column form / wizard** → `mx-auto max-w-2xl pt-4` around one `Card` (CampaignWizard pattern).
|
|
79
|
+
- **Wide / two-column** (form + summary aside) → `mx-auto flex max-w-7xl flex-col gap-6 pt-8 lg:flex-row`, a `Card` per column.
|
|
80
|
+
|
|
81
|
+
## 3. DashboardPage (standard in-app page)
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import {
|
|
85
|
+
DashboardPage,
|
|
86
|
+
DashboardPageContent,
|
|
87
|
+
DashboardPageMain,
|
|
88
|
+
DashboardPageTabs,
|
|
89
|
+
DashboardPageHeader,
|
|
90
|
+
DashboardPageHeaderTitle,
|
|
91
|
+
DashboardPageHeaderActions,
|
|
92
|
+
DashboardPageHeaderAction,
|
|
93
|
+
DashboardSidebarProvider
|
|
94
|
+
} from '@aircall/blocks';
|
|
95
|
+
|
|
96
|
+
<DashboardPage>
|
|
97
|
+
<DashboardSidebarProvider defaultOpen>
|
|
98
|
+
{/* your <DashboardSidebar> */}
|
|
99
|
+
<DashboardPageContent>
|
|
100
|
+
<DashboardPageHeader>
|
|
101
|
+
<DashboardPageHeaderTitle size="lg">Campaigns</DashboardPageHeaderTitle>
|
|
102
|
+
<DashboardPageHeaderActions>
|
|
103
|
+
<DashboardPageHeaderAction variant="default">New campaign</DashboardPageHeaderAction>
|
|
104
|
+
</DashboardPageHeaderActions>
|
|
105
|
+
</DashboardPageHeader>
|
|
106
|
+
{/* optional: <DashboardPageTabs> here, as a sibling of the header */}
|
|
107
|
+
<DashboardPageMain>{/* page content */}</DashboardPageMain>
|
|
108
|
+
</DashboardPageContent>
|
|
109
|
+
</DashboardSidebarProvider>
|
|
110
|
+
</DashboardPage>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`DashboardPageBanner` is available for a page-level banner. The page header itself migrates per `@aircall/blocks#aircall-blocks/migrate-dashboard/page-header`.
|
|
114
|
+
|
|
115
|
+
### Page-level notices (beta/feature/trial banners) → `NotificationQueue`
|
|
116
|
+
|
|
117
|
+
For app/page-level notices (e.g. a "beta feature" banner), prefer the ds **`NotificationQueue`** over an inline `Banner` — it stacks, dedupes, and orders by priority, and renders wherever you place a `NotificationSlot` (e.g. at the very top of the page, above the header). Mechanism (from `packages/ds/src/components/notification-queue.tsx`):
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
import { NotificationQueueProvider, NotificationSlot, useNotification } from '@aircall/ds';
|
|
121
|
+
|
|
122
|
+
// 1. Wrap the screen (or app) once:
|
|
123
|
+
<NotificationQueueProvider>
|
|
124
|
+
<NotificationSlot slot="page" /> {/* render output at the top of the page */}
|
|
125
|
+
{/* …DashboardPage / DashboardStandalonePage… */}
|
|
126
|
+
</NotificationQueueProvider>
|
|
127
|
+
|
|
128
|
+
// 2. Declare the notice (lifecycle-bound) from any descendant:
|
|
129
|
+
useNotification({
|
|
130
|
+
id: 'campaigns-beta',
|
|
131
|
+
slot: 'page',
|
|
132
|
+
priority: 'promotional', // 'error' | 'warning' | 'success' | 'info' | 'promotional'
|
|
133
|
+
render: () => <>You're using a beta feature 🚧 …</>
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Use `useNotification(...)` for persistent/conditional notices (beta banner); `useNotificationQueue().push(...)` for imperative, event-driven ones. `error` priority renders without a dismiss CTA.
|
|
138
|
+
|
|
139
|
+
Source: `packages/ds/src/components/notification-queue.tsx`
|
|
140
|
+
|
|
141
|
+
## 4. Common Mistakes
|
|
142
|
+
|
|
143
|
+
### HIGH — Using `DashboardPage` (with sidebar) for a full-page creation flow
|
|
144
|
+
|
|
145
|
+
Wrong:
|
|
146
|
+
```tsx
|
|
147
|
+
<DashboardPage>
|
|
148
|
+
<DashboardSidebarProvider>{/* sidebar */}
|
|
149
|
+
<DashboardPageContent>{/* Campaign Creation wizard */}</DashboardPageContent>
|
|
150
|
+
</DashboardSidebarProvider>
|
|
151
|
+
</DashboardPage>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Correct:
|
|
155
|
+
```tsx
|
|
156
|
+
<DashboardStandalonePage>
|
|
157
|
+
<DashboardStandalonePageHeader>...</DashboardStandalonePageHeader>
|
|
158
|
+
<DashboardStandalonePageContent>{/* wizard */}</DashboardStandalonePageContent>
|
|
159
|
+
</DashboardStandalonePage>
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Focused flows (creation, Add Contacts) take over the viewport and have no sidebar. Wrapping them in `DashboardPage` renders the dashboard chrome/sidebar around the flow and needlessly pulls in `DashboardSidebarProvider`.
|
|
163
|
+
|
|
164
|
+
Source: `packages/blocks/src/components/dashboard-standalone-page.tsx`
|
|
165
|
+
|
|
166
|
+
### HIGH — Putting the header outside `DashboardPageContent`
|
|
167
|
+
|
|
168
|
+
Wrong:
|
|
169
|
+
```tsx
|
|
170
|
+
<DashboardPage>
|
|
171
|
+
<DashboardPageHeader>...</DashboardPageHeader> {/* direct child of DashboardPage */}
|
|
172
|
+
<DashboardPageContent>...</DashboardPageContent>
|
|
173
|
+
</DashboardPage>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Correct: the header lives **inside** `DashboardPageContent`, alongside `DashboardPageMain`.
|
|
177
|
+
|
|
178
|
+
`DashboardPageContent` owns the header/main grid (and a `:has()` rule manages the header border). A header outside it loses that layout and the border handling.
|
|
179
|
+
|
|
180
|
+
Source: `packages/blocks/src/components/dashboard-page.tsx`
|
|
181
|
+
|
|
182
|
+
### MEDIUM — Nesting `DashboardPageTabs` inside the header
|
|
183
|
+
|
|
184
|
+
Wrong: `<DashboardPageHeader><DashboardPageTabs/></DashboardPageHeader>`.
|
|
185
|
+
|
|
186
|
+
Correct: `DashboardPageTabs` is a **sibling** of `DashboardPageHeader` inside `DashboardPageContent`.
|
|
187
|
+
|
|
188
|
+
`DashboardPageTabs` provides its own `border-b`; nesting it in the header double-borders and misplaces the tab strip.
|
|
189
|
+
|
|
190
|
+
Source: `packages/blocks/src/components/dashboard-page.tsx`
|
|
191
|
+
|
|
192
|
+
### MEDIUM — Re-creating page chrome with raw `div`/`Paper` instead of the shell
|
|
193
|
+
|
|
194
|
+
Wrong: a hand-rolled `<div className="page">` + a `Paper`/`Card` page header.
|
|
195
|
+
|
|
196
|
+
Correct: use the `DashboardPage` / `DashboardStandalonePage` shell so spacing, borders, and the header/content grid match the rest of the dashboard.
|
|
197
|
+
|
|
198
|
+
Source: `packages/blocks/src/components/dashboard-page.tsx`
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: aircall-blocks/migrate-dashboard/data-table
|
|
3
|
+
description: >
|
|
4
|
+
Migrate @dashboard/library LoadMoreTable, LoadMoreStrategy, LoadMoreTableProps,
|
|
5
|
+
and LoadMoreScrollProps to the @aircall/ds DataTable (TanStack Table-backed).
|
|
6
|
+
Load when a file imports LoadMoreTable or LoadMoreStrategy from @dashboard/library.
|
|
7
|
+
type: sub-skill
|
|
8
|
+
library: aircall-blocks
|
|
9
|
+
library_version: "0.5.1"
|
|
10
|
+
requires:
|
|
11
|
+
- aircall-blocks/setup
|
|
12
|
+
- aircall-blocks/migrate-dashboard
|
|
13
|
+
sources:
|
|
14
|
+
- "aircall/hydra:packages/ds/src/index.ts"
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
This skill builds on aircall-blocks/migrate-dashboard.
|
|
18
|
+
|
|
19
|
+
## 1. Architecture change
|
|
20
|
+
|
|
21
|
+
`LoadMoreTable` from `@dashboard/library` was a self-contained wrapper around Tractor's
|
|
22
|
+
`Table` that bolted on a load-more strategy (`button` or `scroll`) and a loading spinner
|
|
23
|
+
overlay. Columns were passed as plain descriptor objects (`{ id, label, renderer? }`).
|
|
24
|
+
|
|
25
|
+
`DataTable` from `@aircall/ds` is built on **TanStack Table v8**. There is no strategy
|
|
26
|
+
enum — pagination mode is controlled by a `loadingState` string and an `onFetchMore`
|
|
27
|
+
callback. Columns are `ColumnDef<TData>[]` from `@tanstack/react-table`. The component
|
|
28
|
+
handles both infinite scroll (via IntersectionObserver sentinel) and initial-load
|
|
29
|
+
skeletons natively.
|
|
30
|
+
|
|
31
|
+
## 2. Target mapping
|
|
32
|
+
|
|
33
|
+
| `@dashboard/library` | `@aircall/ds` |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `LoadMoreTable` | `DataTable` |
|
|
36
|
+
| `LoadMoreStrategy.Button` | No direct equivalent — use a custom "Load more" button rendered outside `DataTable` (see §4b) |
|
|
37
|
+
| `LoadMoreStrategy.Scroll` | `onFetchMore` + `hasMore` (built-in sentinel) |
|
|
38
|
+
| `loading={true}` (initial) | `loadingState="loading"` |
|
|
39
|
+
| `loadingMore={true}` | `loadingState="loadingMore"` |
|
|
40
|
+
| `hasMore` | `hasMore` |
|
|
41
|
+
| `onLoadMore` | `onFetchMore` |
|
|
42
|
+
| `columns[n].id` | `ColumnDef.accessorKey` (or `id` + `accessorFn`) |
|
|
43
|
+
| `columns[n].label` | `ColumnDef.header` |
|
|
44
|
+
| `columns[n].renderer` | `ColumnDef.cell: ({ row }) => row.original.<field>` |
|
|
45
|
+
| `noDataMessage` | `emptyState` (accepts `ReactNode`) |
|
|
46
|
+
| `data-test` | `data-test` (still forwarded via `...props`) |
|
|
47
|
+
| `buttonText` / `loadingText` | Not a prop — render your own button outside `DataTable` |
|
|
48
|
+
| `scrollThreshold` | Fixed 200 px look-ahead in `DataTable` sentinel — not configurable |
|
|
49
|
+
| `h` / flex layout props | Use `fillHeight` prop + flex parent (`flex flex-col min-h-0`) |
|
|
50
|
+
|
|
51
|
+
## 3. Verified DS exports (`packages/ds/src/index.ts`)
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
DataTable, type DataTableLoadingState
|
|
55
|
+
Empty, EmptyContent, EmptyTitle, EmptyDescription
|
|
56
|
+
Spinner
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`@aircall/ds` **re-exports** the TanStack Table types you author — `ColumnDef`, `SortingState`, `RowSelectionState`, `OnChangeFn` — so **import them from `@aircall/ds`**, not `@tanstack/react-table`. The react-table runtime ships as a regular `@aircall/ds` dependency (auto-installed; `DataTable` owns the table instance internally), so there is nothing to install and no version to keep in sync.
|
|
60
|
+
|
|
61
|
+
## 4. Before / After
|
|
62
|
+
|
|
63
|
+
### 4a. Scroll strategy (infinite scroll)
|
|
64
|
+
|
|
65
|
+
**Before (`@dashboard/library`):**
|
|
66
|
+
```tsx
|
|
67
|
+
import { useState } from 'react';
|
|
68
|
+
import { LoadMoreTable, LoadMoreStrategy } from '@dashboard/library';
|
|
69
|
+
|
|
70
|
+
type Row = { id: string; name: string; email: string };
|
|
71
|
+
|
|
72
|
+
const columns = [
|
|
73
|
+
{ id: 'name', label: 'Name' },
|
|
74
|
+
{ id: 'email', label: 'Email', renderer: ({ email }: Row) => <a href={`mailto:${email}`}>{email}</a> },
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
function PeopleTable() {
|
|
78
|
+
const [data, setData] = useState<Row[]>(initialPage);
|
|
79
|
+
const [loading, setLoading] = useState(false);
|
|
80
|
+
const [loadingMore, setLoadingMore] = useState(false);
|
|
81
|
+
const [hasMore, setHasMore] = useState(true);
|
|
82
|
+
|
|
83
|
+
const handleLoadMore = async () => {
|
|
84
|
+
setLoadingMore(true);
|
|
85
|
+
const next = await fetchNextPage();
|
|
86
|
+
setData(prev => [...prev, ...next.rows]);
|
|
87
|
+
setHasMore(next.hasMore);
|
|
88
|
+
setLoadingMore(false);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<LoadMoreTable
|
|
93
|
+
data={data}
|
|
94
|
+
columns={columns}
|
|
95
|
+
hasMore={hasMore}
|
|
96
|
+
loading={loading}
|
|
97
|
+
loadingMore={loadingMore}
|
|
98
|
+
h="420px"
|
|
99
|
+
onLoadMore={handleLoadMore}
|
|
100
|
+
strategy={LoadMoreStrategy.Scroll}
|
|
101
|
+
noDataMessage="No people found."
|
|
102
|
+
data-test="people-table"
|
|
103
|
+
/>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**After (`@aircall/ds`):**
|
|
109
|
+
```tsx
|
|
110
|
+
import { useState } from 'react';
|
|
111
|
+
import { DataTable, type DataTableLoadingState, Empty, EmptyContent, EmptyTitle } from '@aircall/ds';
|
|
112
|
+
import type { ColumnDef } from '@aircall/ds';
|
|
113
|
+
|
|
114
|
+
type Row = { id: string; name: string; email: string };
|
|
115
|
+
|
|
116
|
+
const columns: ColumnDef<Row>[] = [
|
|
117
|
+
{ accessorKey: 'name', header: 'Name' },
|
|
118
|
+
{
|
|
119
|
+
accessorKey: 'email',
|
|
120
|
+
header: 'Email',
|
|
121
|
+
cell: ({ row }) => <a href={`mailto:${row.original.email}`}>{row.original.email}</a>,
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
function PeopleTable() {
|
|
126
|
+
const [data, setData] = useState<Row[]>(initialPage);
|
|
127
|
+
const [loadingState, setLoadingState] = useState<DataTableLoadingState>('idle');
|
|
128
|
+
const [hasMore, setHasMore] = useState(true);
|
|
129
|
+
|
|
130
|
+
const handleFetchMore = async () => {
|
|
131
|
+
setLoadingState('loadingMore');
|
|
132
|
+
const next = await fetchNextPage();
|
|
133
|
+
setData(prev => [...prev, ...next.rows]);
|
|
134
|
+
setHasMore(next.hasMore);
|
|
135
|
+
setLoadingState('idle');
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div className="flex flex-col min-h-0" style={{ height: 420 }}>
|
|
140
|
+
<DataTable
|
|
141
|
+
data={data}
|
|
142
|
+
columns={columns}
|
|
143
|
+
getRowId={row => row.id}
|
|
144
|
+
loadingState={loadingState}
|
|
145
|
+
hasMore={hasMore}
|
|
146
|
+
onFetchMore={handleFetchMore}
|
|
147
|
+
fillHeight
|
|
148
|
+
emptyState={
|
|
149
|
+
<Empty>
|
|
150
|
+
<EmptyContent>
|
|
151
|
+
<EmptyTitle>No people found</EmptyTitle>
|
|
152
|
+
</EmptyContent>
|
|
153
|
+
</Empty>
|
|
154
|
+
}
|
|
155
|
+
data-test="people-table"
|
|
156
|
+
/>
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Key changes:
|
|
163
|
+
- `loading` / `loadingMore` booleans collapse into a single `loadingState` discriminated string
|
|
164
|
+
- `strategy={LoadMoreStrategy.Scroll}` + `onLoadMore` → `onFetchMore` + `hasMore` (sentinel is built-in)
|
|
165
|
+
- Column descriptors (`{ id, label, renderer }`) → `ColumnDef<TData>[]` with `accessorKey`, `header`, and `cell`
|
|
166
|
+
- `h="420px"` (Tractor Flex prop) → wrap in a flex container with a bounded height + pass `fillHeight`
|
|
167
|
+
- `noDataMessage` string → `emptyState` ReactNode
|
|
168
|
+
|
|
169
|
+
### 4b. Button strategy
|
|
170
|
+
|
|
171
|
+
`LoadMoreStrategy.Button` has no direct prop equivalent in `DataTable`. Render the
|
|
172
|
+
"Load more" button below the table instead:
|
|
173
|
+
|
|
174
|
+
**Before (`@dashboard/library`):**
|
|
175
|
+
```tsx
|
|
176
|
+
import { LoadMoreTable, LoadMoreStrategy } from '@dashboard/library';
|
|
177
|
+
|
|
178
|
+
<LoadMoreTable
|
|
179
|
+
data={data}
|
|
180
|
+
columns={columns}
|
|
181
|
+
hasMore={hasMore}
|
|
182
|
+
loading={loading}
|
|
183
|
+
loadingMore={loadingMore}
|
|
184
|
+
buttonText="Load more"
|
|
185
|
+
loadingText="Loading…"
|
|
186
|
+
onLoadMore={handleLoadMore}
|
|
187
|
+
strategy={LoadMoreStrategy.Button}
|
|
188
|
+
/>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**After (`@aircall/ds`):**
|
|
192
|
+
```tsx
|
|
193
|
+
import { DataTable, type DataTableLoadingState, Spinner, Button } from '@aircall/ds';
|
|
194
|
+
import type { ColumnDef } from '@aircall/ds';
|
|
195
|
+
|
|
196
|
+
// Button is from @aircall/ds; import it alongside DataTable.
|
|
197
|
+
<div className="flex flex-col gap-4">
|
|
198
|
+
<DataTable
|
|
199
|
+
data={data}
|
|
200
|
+
columns={columns}
|
|
201
|
+
loadingState={loadingState === 'loading' ? 'loading' : 'idle'}
|
|
202
|
+
/>
|
|
203
|
+
{hasMore && (
|
|
204
|
+
<div className="flex justify-center">
|
|
205
|
+
<Button
|
|
206
|
+
variant="outline"
|
|
207
|
+
onClick={handleLoadMore}
|
|
208
|
+
disabled={loadingState === 'loadingMore'}
|
|
209
|
+
>
|
|
210
|
+
{loadingState === 'loadingMore' ? <Spinner /> : 'Load more'}
|
|
211
|
+
</Button>
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Key changes:
|
|
218
|
+
- `buttonText` / `loadingText` / `loadingMore` are not props on `DataTable` — drive the
|
|
219
|
+
button's label and disabled state from your own `loadingState` variable
|
|
220
|
+
- `Spinner` and `Button` are imported from `@aircall/ds` (same package as `DataTable`)
|
|
221
|
+
|
|
222
|
+
### 4c. Initial loading state
|
|
223
|
+
|
|
224
|
+
**Before (`@dashboard/library`):**
|
|
225
|
+
```tsx
|
|
226
|
+
import { LoadMoreTable } from '@dashboard/library';
|
|
227
|
+
|
|
228
|
+
<LoadMoreTable
|
|
229
|
+
data={data ?? null}
|
|
230
|
+
columns={columns}
|
|
231
|
+
hasMore={false}
|
|
232
|
+
loading={isLoading}
|
|
233
|
+
onLoadMore={() => {}}
|
|
234
|
+
/>
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**After (`@aircall/ds`):**
|
|
238
|
+
```tsx
|
|
239
|
+
import { DataTable } from '@aircall/ds';
|
|
240
|
+
import type { ColumnDef } from '@aircall/ds';
|
|
241
|
+
|
|
242
|
+
<DataTable
|
|
243
|
+
data={data ?? []}
|
|
244
|
+
columns={columns}
|
|
245
|
+
loadingState={isLoading ? 'loading' : 'idle'}
|
|
246
|
+
/>
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Key changes:
|
|
250
|
+
- `data` is always `TData[]`; pass `[]` while loading (skeleton rows replace the empty body)
|
|
251
|
+
- `loading={true}` → `loadingState="loading"` shows skeleton rows automatically
|
|
252
|
+
- No `hasMore` or `onLoadMore` needed for a non-paginated table
|
|
253
|
+
|
|
254
|
+
## 5. Common mistakes
|
|
255
|
+
|
|
256
|
+
### Mistake 1 — Passing `strategy` or `onLoadMore` to `DataTable`
|
|
257
|
+
|
|
258
|
+
```tsx
|
|
259
|
+
// Wrong — DataTable has no strategy or onLoadMore prop; they are silently ignored
|
|
260
|
+
import { DataTable } from '@aircall/ds';
|
|
261
|
+
|
|
262
|
+
<DataTable
|
|
263
|
+
data={data}
|
|
264
|
+
columns={columns}
|
|
265
|
+
strategy="scroll"
|
|
266
|
+
onLoadMore={handleLoadMore}
|
|
267
|
+
/>
|
|
268
|
+
|
|
269
|
+
// Correct — use onFetchMore for scroll-based pagination
|
|
270
|
+
<DataTable
|
|
271
|
+
data={data}
|
|
272
|
+
columns={columns}
|
|
273
|
+
hasMore={hasMore}
|
|
274
|
+
onFetchMore={handleFetchMore}
|
|
275
|
+
/>
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
`LoadMoreStrategy` and `onLoadMore` are `@dashboard/library` concepts. `DataTable` exposes
|
|
279
|
+
`onFetchMore` (triggers when the IntersectionObserver sentinel enters the viewport) and
|
|
280
|
+
`hasMore` (hides the sentinel when false). There is no button-strategy prop — render your
|
|
281
|
+
own button outside the table for that UX pattern.
|
|
282
|
+
|
|
283
|
+
Source: `packages/ds/src/components/data-table.tsx`
|
|
284
|
+
|
|
285
|
+
### Mistake 2 — Keeping `loading` / `loadingMore` as separate boolean props
|
|
286
|
+
|
|
287
|
+
```tsx
|
|
288
|
+
// Wrong — DataTable has no loading or loadingMore props; they are silently ignored
|
|
289
|
+
<DataTable
|
|
290
|
+
data={data}
|
|
291
|
+
columns={columns}
|
|
292
|
+
loading={isLoading}
|
|
293
|
+
loadingMore={isFetchingMore}
|
|
294
|
+
/>
|
|
295
|
+
|
|
296
|
+
// Correct — collapse both booleans into a single loadingState string
|
|
297
|
+
<DataTable
|
|
298
|
+
data={data}
|
|
299
|
+
columns={columns}
|
|
300
|
+
loadingState={isLoading ? 'loading' : isFetchingMore ? 'loadingMore' : 'idle'}
|
|
301
|
+
/>
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
DS replaced the dual-boolean pattern with a discriminated `loadingState` string so each
|
|
305
|
+
async phase renders the right UX: `'loading'` shows skeleton rows (initial load), while
|
|
306
|
+
`'loadingMore'` appends a spinner row at the bottom (pagination). Passing `loading` or
|
|
307
|
+
`loadingMore` as props is silently ignored and the table will not show any loading indicator.
|
|
308
|
+
|
|
309
|
+
Source: `packages/ds/src/components/data-table.tsx`
|
|
310
|
+
|
|
311
|
+
### Mistake 3 — Using column descriptor objects instead of `ColumnDef`
|
|
312
|
+
|
|
313
|
+
```tsx
|
|
314
|
+
// Wrong — { id, label, renderer } is the @dashboard/library TableColumn shape;
|
|
315
|
+
// DataTable does not read these fields
|
|
316
|
+
import { DataTable } from '@aircall/ds';
|
|
317
|
+
|
|
318
|
+
const columns = [
|
|
319
|
+
{ id: 'name', label: 'Name' },
|
|
320
|
+
{ id: 'email', label: 'Email', renderer: ({ email }) => <a href={`mailto:${email}`}>{email}</a> },
|
|
321
|
+
];
|
|
322
|
+
|
|
323
|
+
<DataTable data={data} columns={columns} />
|
|
324
|
+
|
|
325
|
+
// Correct — use ColumnDef from @tanstack/react-table
|
|
326
|
+
import type { ColumnDef } from '@aircall/ds';
|
|
327
|
+
|
|
328
|
+
type Row = { id: string; name: string; email: string };
|
|
329
|
+
|
|
330
|
+
const columns: ColumnDef<Row>[] = [
|
|
331
|
+
{ accessorKey: 'name', header: 'Name' },
|
|
332
|
+
{
|
|
333
|
+
accessorKey: 'email',
|
|
334
|
+
header: 'Email',
|
|
335
|
+
cell: ({ row }) => <a href={`mailto:${row.original.email}`}>{row.original.email}</a>,
|
|
336
|
+
},
|
|
337
|
+
];
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
`DataTable` is built on TanStack Table v8. It reads `ColumnDef` fields (`accessorKey`,
|
|
341
|
+
`header`, `cell`, `enableSorting`, etc.) — not `id`, `label`, or `renderer`. Passing the
|
|
342
|
+
old descriptor shape compiles (TypeScript can't narrow the column union tightly), but
|
|
343
|
+
all columns will render blank cells because no accessor is registered.
|
|
344
|
+
|
|
345
|
+
Source: `packages/ds/src/components/data-table.tsx`
|
|
346
|
+
|
|
347
|
+
### Mistake 4 — Passing `noDataMessage` instead of `emptyState`
|
|
348
|
+
|
|
349
|
+
```tsx
|
|
350
|
+
// Wrong — noDataMessage is a @dashboard/library / Tractor prop; DataTable ignores it
|
|
351
|
+
<DataTable data={[]} columns={columns} noDataMessage="No records found." />
|
|
352
|
+
|
|
353
|
+
// Correct — emptyState accepts any ReactNode; use the Empty compound for rich states
|
|
354
|
+
import { DataTable, Empty, EmptyContent, EmptyTitle, EmptyDescription } from '@aircall/ds';
|
|
355
|
+
|
|
356
|
+
<DataTable
|
|
357
|
+
data={[]}
|
|
358
|
+
columns={columns}
|
|
359
|
+
emptyState={
|
|
360
|
+
<Empty>
|
|
361
|
+
<EmptyContent>
|
|
362
|
+
<EmptyTitle>No records found</EmptyTitle>
|
|
363
|
+
<EmptyDescription>Try adjusting your filters.</EmptyDescription>
|
|
364
|
+
</EmptyContent>
|
|
365
|
+
</Empty>
|
|
366
|
+
}
|
|
367
|
+
/>
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
`DataTable` requires an `emptyState` prop; `noDataMessage` is not defined on `DataTable`
|
|
371
|
+
and is silently ignored — the table body renders nothing when data is empty. For plain
|
|
372
|
+
text wrap it in `<p className="text-sm text-muted-foreground">`. For a full empty state
|
|
373
|
+
with an icon and call-to-action, compose with the `Empty` family from `@aircall/ds`.
|
|
374
|
+
|
|
375
|
+
Source: `packages/ds/src/components/data-table.tsx`
|
|
376
|
+
|
|
377
|
+
### Mistake 5 — Passing height as a Tractor prop instead of using `fillHeight`
|
|
378
|
+
|
|
379
|
+
```tsx
|
|
380
|
+
// Wrong — h / w are Tractor Flex props; DataTable does not accept them
|
|
381
|
+
<DataTable data={data} columns={columns} h="420px" />
|
|
382
|
+
|
|
383
|
+
// Correct — constrain the parent height with a flex container and pass fillHeight
|
|
384
|
+
<div className="flex flex-col min-h-0" style={{ height: 420 }}>
|
|
385
|
+
<DataTable data={data} columns={columns} fillHeight />
|
|
386
|
+
</div>
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
`LoadMoreTable` forwarded layout props (`h`, `w`, `flex`, etc.) to the Tractor `Flex`
|
|
390
|
+
wrapper it rendered internally. `DataTable` has no such props. Use `fillHeight` to pin
|
|
391
|
+
the header and scroll the body internally — the parent must provide a bounded height via
|
|
392
|
+
a flex column (`flex flex-col` + `min-h-0`). Without a bounded parent, `fillHeight` has
|
|
393
|
+
no effect and the table grows to its natural height.
|
|
394
|
+
|
|
395
|
+
Source: `packages/ds/src/components/data-table.tsx`
|