@aircall/blocks 0.5.1 → 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 +81 -24
- package/dist/index.js +493 -113
- package/package.json +20 -4
- 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,487 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: aircall-blocks/migrate-dashboard/combobox
|
|
3
|
+
description: >
|
|
4
|
+
Migrate @dashboard/library search-selects to @aircall/ds Combobox — multi-select
|
|
5
|
+
(MultiSearchSelect, MultiInlineSearchSelect, MultiSelectOption, multi MultiSelect) via
|
|
6
|
+
the `multiple` prop, and single-select (SingleSearchSelect, SearchSelect) by omitting
|
|
7
|
+
it. Load when a file imports any *SearchSelect / MultiSelect from @dashboard/library.
|
|
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/ds/src/index.ts"
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
This skill builds on aircall-blocks/migrate-dashboard.
|
|
19
|
+
|
|
20
|
+
## 1. Component mapping
|
|
21
|
+
|
|
22
|
+
| @dashboard/library | @aircall/ds |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| `MultiSearchSelect` (root with search input + dropdown) | `Combobox` (root, `multiple` prop) + `ComboboxChips` + `ComboboxChipsInput` + `ComboboxContent` + `ComboboxList` + `ComboboxItem` |
|
|
25
|
+
| `MultiInlineSearchSelect` (root with inline chips + search box) | `Combobox` (root, `multiple` prop) + `ComboboxChips` + `ComboboxChipsInput` + `ComboboxContent` + `ComboboxList` + `ComboboxItem` |
|
|
26
|
+
| `MultiSelectOption` (option shape `{ value, label }`) | Consumer-defined plain object — DS has no equivalent type; define `{ value: string; label: string }` locally |
|
|
27
|
+
| `options` prop (array of options for the list) | `items` prop on `Combobox` |
|
|
28
|
+
| `onSelect` prop (receives `selectedKeys: string[]`) | `onValueChange` prop on `Combobox` (receives option objects, not raw keys) |
|
|
29
|
+
| `selectedKeys` / `defaultSelectedKeys` prop | `value` / `defaultValue` prop on `Combobox` (option objects, not strings) |
|
|
30
|
+
| `selectionMode="multiple"` | `multiple` boolean prop on `Combobox` |
|
|
31
|
+
| _single-select_ (`SingleSearchSelect`, `SearchSelect`, single-value `MultiSelect`) | **omit** `multiple` — `Combobox` is single by default. Use `ComboboxInput` instead of `ComboboxChips`/`ComboboxChipsInput`; `value`/`onValueChange` carry one option object (or `null` when cleared) |
|
|
32
|
+
| `renderTag` prop (custom chip renderer) | `ComboboxValue` render-prop inside `ComboboxChips`; render `ComboboxChip` per item |
|
|
33
|
+
| `renderItem` prop (custom list-row renderer) | `children` of `ComboboxItem` |
|
|
34
|
+
| `renderItemPrefix` / `renderItemSuffix` | Inline JSX children of `ComboboxItem` (before / after the label) |
|
|
35
|
+
| `renderItemDescription` | Inline `ItemDescription` children of `ComboboxItem` |
|
|
36
|
+
| `emptyLabel` prop | `ComboboxEmpty` children |
|
|
37
|
+
| `placeholder` prop | `placeholder` on `ComboboxChipsInput` |
|
|
38
|
+
| `loading` prop | Conditional render above `ComboboxList` (no built-in loading state) |
|
|
39
|
+
| `disabled` prop | `disabled` prop on `Combobox` |
|
|
40
|
+
| `onSearch` / `debounceDuration` | Caller-owned debounced state; filter `items` before passing to `Combobox` |
|
|
41
|
+
| `hideClearAll` / `onClearAll` | No direct equivalent — `ComboboxChip` ships individual removals; a clear-all button is custom UI beside `ComboboxChips` |
|
|
42
|
+
| `hideSelectedOptions` | Filter `items` in consumer state before passing to `Combobox` |
|
|
43
|
+
| `maxMenuHeight` / `maxSearchBoxHeight` | `className` on `ComboboxContent` / `ComboboxChips` (Tailwind `max-h-*`) |
|
|
44
|
+
|
|
45
|
+
## 2. Imports
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
// All multi-select combobox primitives from @aircall/ds
|
|
49
|
+
import {
|
|
50
|
+
Combobox,
|
|
51
|
+
ComboboxChip,
|
|
52
|
+
ComboboxChips,
|
|
53
|
+
ComboboxChipsInput,
|
|
54
|
+
ComboboxContent,
|
|
55
|
+
ComboboxEmpty,
|
|
56
|
+
ComboboxItem,
|
|
57
|
+
ComboboxList,
|
|
58
|
+
ComboboxValue,
|
|
59
|
+
useComboboxAnchor
|
|
60
|
+
} from '@aircall/ds';
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Note: `ComboboxMultiSelect` is a recipe in `src/recipes/` and is intentionally **not** exported from `@aircall/ds`. Wire the primitives above directly.
|
|
64
|
+
|
|
65
|
+
## 3. Before / After
|
|
66
|
+
|
|
67
|
+
### 3a. MultiSearchSelect — dropdown multi-select with search
|
|
68
|
+
|
|
69
|
+
**Before (`@dashboard/library`):**
|
|
70
|
+
```tsx
|
|
71
|
+
import { MultiSearchSelect, MultiSelectOption } from '@dashboard/library';
|
|
72
|
+
|
|
73
|
+
interface TeamOption extends MultiSelectOption<string> {
|
|
74
|
+
emoji: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const options: TeamOption[] = [
|
|
78
|
+
{ value: 'eng', label: 'Engineering', emoji: '🛠' },
|
|
79
|
+
{ value: 'sales', label: 'Sales', emoji: '📈' },
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
function TeamPicker() {
|
|
83
|
+
const [selected, setSelected] = useState<string[]>(['eng']);
|
|
84
|
+
const [loading, setLoading] = useState(false);
|
|
85
|
+
|
|
86
|
+
const handleSearch = async (query: string) => {
|
|
87
|
+
// fetch options remotely
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<MultiSearchSelect<TeamOption>
|
|
92
|
+
options={options}
|
|
93
|
+
loading={loading}
|
|
94
|
+
defaultSelectedKeys={['eng']}
|
|
95
|
+
onSearch={handleSearch}
|
|
96
|
+
onSelect={setSelected}
|
|
97
|
+
renderItemPrefix={(o) => o.emoji}
|
|
98
|
+
texts={{ placeholder: 'Select teams', item: 'team', items: 'teams' }}
|
|
99
|
+
debounceDuration={300}
|
|
100
|
+
/>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**After (`@aircall/ds`):**
|
|
106
|
+
```tsx
|
|
107
|
+
import {
|
|
108
|
+
Combobox,
|
|
109
|
+
ComboboxChip,
|
|
110
|
+
ComboboxChips,
|
|
111
|
+
ComboboxChipsInput,
|
|
112
|
+
ComboboxContent,
|
|
113
|
+
ComboboxEmpty,
|
|
114
|
+
ComboboxItem,
|
|
115
|
+
ComboboxList,
|
|
116
|
+
ComboboxValue,
|
|
117
|
+
useComboboxAnchor
|
|
118
|
+
} from '@aircall/ds';
|
|
119
|
+
import { useMemo, useState, useRef } from 'react';
|
|
120
|
+
|
|
121
|
+
type TeamOption = { value: string; label: string; emoji: string };
|
|
122
|
+
|
|
123
|
+
const allOptions: TeamOption[] = [
|
|
124
|
+
{ value: 'eng', label: 'Engineering', emoji: '🛠' },
|
|
125
|
+
{ value: 'sales', label: 'Sales', emoji: '📈' },
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
function TeamPicker() {
|
|
129
|
+
const [selected, setSelected] = useState<TeamOption[]>([allOptions[0]]);
|
|
130
|
+
const [filteredOptions, setFilteredOptions] = useState(allOptions);
|
|
131
|
+
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
132
|
+
|
|
133
|
+
const anchor = useComboboxAnchor();
|
|
134
|
+
|
|
135
|
+
const handleSearch = (query: string) => {
|
|
136
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
137
|
+
debounceRef.current = setTimeout(async () => {
|
|
138
|
+
// fetch or filter remotely, then update filteredOptions
|
|
139
|
+
setFilteredOptions(
|
|
140
|
+
allOptions.filter((o) => o.label.toLowerCase().includes(query.toLowerCase()))
|
|
141
|
+
);
|
|
142
|
+
}, 300);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<Combobox
|
|
147
|
+
multiple
|
|
148
|
+
autoHighlight
|
|
149
|
+
items={filteredOptions}
|
|
150
|
+
itemToStringValue={(item: TeamOption) => item.value}
|
|
151
|
+
value={selected}
|
|
152
|
+
onValueChange={setSelected}
|
|
153
|
+
>
|
|
154
|
+
<ComboboxChips ref={anchor}>
|
|
155
|
+
<ComboboxValue>
|
|
156
|
+
{(values: TeamOption[]) => (
|
|
157
|
+
<>
|
|
158
|
+
{values.map((o) => (
|
|
159
|
+
<ComboboxChip key={o.value}>{o.label}</ComboboxChip>
|
|
160
|
+
))}
|
|
161
|
+
<ComboboxChipsInput
|
|
162
|
+
placeholder={selected.length === 0 ? 'Select teams' : ''}
|
|
163
|
+
onChange={(e) => handleSearch(e.target.value)}
|
|
164
|
+
/>
|
|
165
|
+
</>
|
|
166
|
+
)}
|
|
167
|
+
</ComboboxValue>
|
|
168
|
+
</ComboboxChips>
|
|
169
|
+
<ComboboxContent anchor={anchor}>
|
|
170
|
+
<ComboboxEmpty>No teams found.</ComboboxEmpty>
|
|
171
|
+
<ComboboxList>
|
|
172
|
+
{(item: TeamOption) => (
|
|
173
|
+
<ComboboxItem key={item.value} value={item}>
|
|
174
|
+
<span>{item.emoji}</span>
|
|
175
|
+
{item.label}
|
|
176
|
+
</ComboboxItem>
|
|
177
|
+
)}
|
|
178
|
+
</ComboboxList>
|
|
179
|
+
</ComboboxContent>
|
|
180
|
+
</Combobox>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Key changes:
|
|
186
|
+
- `options` → `items` on `Combobox`.
|
|
187
|
+
- `selectionMode="multiple"` → `multiple` boolean.
|
|
188
|
+
- `selectedKeys: string[]` → `value: TeamOption[]` (full option objects, not raw keys).
|
|
189
|
+
- `onSelect(keys)` → `onValueChange(options)` — caller must derive keys from `options.map(o => o.value)` if needed downstream.
|
|
190
|
+
- `onSearch` + `debounceDuration` → caller-owned debounced `onChange` on `ComboboxChipsInput`; filter `items` in state.
|
|
191
|
+
- `renderItemPrefix` → inline JSX sibling before the label inside `ComboboxItem`.
|
|
192
|
+
- `texts.placeholder` → `placeholder` on `ComboboxChipsInput`.
|
|
193
|
+
- `loading` → conditional render; DS has no built-in loading state on Combobox.
|
|
194
|
+
|
|
195
|
+
### 3b. MultiInlineSearchSelect — inline chips with search
|
|
196
|
+
|
|
197
|
+
**Before (`@dashboard/library`):**
|
|
198
|
+
```tsx
|
|
199
|
+
import { MultiInlineSearchSelect } from '@dashboard/library';
|
|
200
|
+
|
|
201
|
+
type UserOption = { value: string; label: string; email: string };
|
|
202
|
+
|
|
203
|
+
function UserPicker() {
|
|
204
|
+
const [selected, setSelected] = useState<string[]>([]);
|
|
205
|
+
const [options, setOptions] = useState<UserOption[]>(allUsers);
|
|
206
|
+
|
|
207
|
+
const handleSearch = (text: string) => {
|
|
208
|
+
setOptions(allUsers.filter((u) => u.label.includes(text)));
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<MultiInlineSearchSelect<UserOption>
|
|
213
|
+
placeholder="Search users..."
|
|
214
|
+
options={options}
|
|
215
|
+
selectedKeys={selected}
|
|
216
|
+
loading={false}
|
|
217
|
+
onSelect={setSelected}
|
|
218
|
+
onSearch={handleSearch}
|
|
219
|
+
renderItemDescription={(o) => o.email}
|
|
220
|
+
emptyLabel="No users found"
|
|
221
|
+
/>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**After (`@aircall/ds`):**
|
|
227
|
+
```tsx
|
|
228
|
+
import {
|
|
229
|
+
Combobox,
|
|
230
|
+
ComboboxChip,
|
|
231
|
+
ComboboxChips,
|
|
232
|
+
ComboboxChipsInput,
|
|
233
|
+
ComboboxContent,
|
|
234
|
+
ComboboxEmpty,
|
|
235
|
+
ComboboxItem,
|
|
236
|
+
ComboboxList,
|
|
237
|
+
ComboboxValue,
|
|
238
|
+
useComboboxAnchor,
|
|
239
|
+
ItemDescription
|
|
240
|
+
} from '@aircall/ds';
|
|
241
|
+
|
|
242
|
+
type UserOption = { value: string; label: string; email: string };
|
|
243
|
+
|
|
244
|
+
function UserPicker() {
|
|
245
|
+
const [selected, setSelected] = useState<UserOption[]>([]);
|
|
246
|
+
const anchor = useComboboxAnchor();
|
|
247
|
+
|
|
248
|
+
// Base UI filters `items` internally as the user types — no manual filter needed
|
|
249
|
+
// for local data. For remote data, see the async pattern in §3a.
|
|
250
|
+
return (
|
|
251
|
+
<Combobox
|
|
252
|
+
multiple
|
|
253
|
+
autoHighlight
|
|
254
|
+
items={allUsers}
|
|
255
|
+
itemToStringValue={(item: UserOption) => item.value}
|
|
256
|
+
value={selected}
|
|
257
|
+
onValueChange={setSelected}
|
|
258
|
+
>
|
|
259
|
+
<ComboboxChips ref={anchor}>
|
|
260
|
+
<ComboboxValue>
|
|
261
|
+
{(values: UserOption[]) => (
|
|
262
|
+
<>
|
|
263
|
+
{values.map((u) => (
|
|
264
|
+
<ComboboxChip key={u.value}>{u.label}</ComboboxChip>
|
|
265
|
+
))}
|
|
266
|
+
<ComboboxChipsInput
|
|
267
|
+
placeholder={selected.length === 0 ? 'Search users...' : ''}
|
|
268
|
+
/>
|
|
269
|
+
</>
|
|
270
|
+
)}
|
|
271
|
+
</ComboboxValue>
|
|
272
|
+
</ComboboxChips>
|
|
273
|
+
<ComboboxContent anchor={anchor}>
|
|
274
|
+
<ComboboxEmpty>No users found</ComboboxEmpty>
|
|
275
|
+
<ComboboxList>
|
|
276
|
+
{(item: UserOption) => (
|
|
277
|
+
<ComboboxItem key={item.value} value={item}>
|
|
278
|
+
{item.label}
|
|
279
|
+
<ItemDescription>{item.email}</ItemDescription>
|
|
280
|
+
</ComboboxItem>
|
|
281
|
+
)}
|
|
282
|
+
</ComboboxList>
|
|
283
|
+
</ComboboxContent>
|
|
284
|
+
</Combobox>
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Key changes:
|
|
290
|
+
- `selectedKeys: string[]` controlled prop → `value: UserOption[]` (objects).
|
|
291
|
+
- `onSelect(keys)` → `onValueChange(options)`.
|
|
292
|
+
- `onSearch` (local filtering) → omitted; Base UI filters `items` internally as the user types in `ComboboxChipsInput`. For remote async data, use the debounced fetch pattern from §3a.
|
|
293
|
+
- `renderItemDescription` → `ItemDescription` from `@aircall/ds` as a child of `ComboboxItem`.
|
|
294
|
+
- `emptyLabel` → `ComboboxEmpty` children.
|
|
295
|
+
- `hideSelectedOptions` logic → pre-filter `items` before passing to `Combobox`.
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## 4. Common mistakes
|
|
300
|
+
|
|
301
|
+
### Mistake 1 — Using `selectedKeys: string[]` instead of `value: Option[]`
|
|
302
|
+
|
|
303
|
+
Wrong block:
|
|
304
|
+
```tsx
|
|
305
|
+
<Combobox multiple items={options} selectedKeys={['eng', 'sales']} onSelect={setKeys}>
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Correct block:
|
|
309
|
+
```tsx
|
|
310
|
+
<Combobox
|
|
311
|
+
multiple
|
|
312
|
+
items={options}
|
|
313
|
+
value={[{ value: 'eng', label: 'Engineering' }, { value: 'sales', label: 'Sales' }]}
|
|
314
|
+
onValueChange={setSelected}
|
|
315
|
+
>
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
`Combobox` from `@aircall/ds` is a Base UI primitive. Its `value` / `onValueChange` carry full option objects, not raw string keys. Passing `selectedKeys` is a no-op (unknown prop silently ignored); the selection state will never be controlled.
|
|
319
|
+
|
|
320
|
+
Source: `packages/ds/src/components/combobox.tsx`
|
|
321
|
+
|
|
322
|
+
### Mistake 2 — Importing ComboboxMultiSelect from @aircall/ds
|
|
323
|
+
|
|
324
|
+
Wrong block:
|
|
325
|
+
```tsx
|
|
326
|
+
import { ComboboxMultiSelect } from '@aircall/ds';
|
|
327
|
+
|
|
328
|
+
<ComboboxMultiSelect options={options} placeholder="Select..." />
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Correct block:
|
|
332
|
+
```tsx
|
|
333
|
+
import {
|
|
334
|
+
Combobox,
|
|
335
|
+
ComboboxChips,
|
|
336
|
+
ComboboxChipsInput,
|
|
337
|
+
ComboboxContent,
|
|
338
|
+
ComboboxList,
|
|
339
|
+
ComboboxItem,
|
|
340
|
+
ComboboxValue,
|
|
341
|
+
ComboboxChip,
|
|
342
|
+
useComboboxAnchor
|
|
343
|
+
} from '@aircall/ds';
|
|
344
|
+
|
|
345
|
+
// Wire primitives directly (see §3 above)
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
`ComboboxMultiSelect` lives in `packages/ds/src/recipes/` and is intentionally excluded from `@aircall/ds`'s public `index.ts`. It is a copy-paste template, not a published export. The import resolves to `undefined` at runtime, crashing silently.
|
|
349
|
+
|
|
350
|
+
Source: `packages/ds/src/components/combobox.tsx`
|
|
351
|
+
|
|
352
|
+
### Mistake 3 — Rendering `<ComboboxClear>` as a JSX element
|
|
353
|
+
|
|
354
|
+
Wrong block:
|
|
355
|
+
```tsx
|
|
356
|
+
import { ComboboxClear } from '@aircall/ds';
|
|
357
|
+
|
|
358
|
+
// inside ComboboxChips:
|
|
359
|
+
<ComboboxClear onClick={onClearAll} />
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Correct block:
|
|
363
|
+
```tsx
|
|
364
|
+
// ComboboxClear is type-only in @aircall/ds.
|
|
365
|
+
// Use a plain button for a custom clear-all action:
|
|
366
|
+
<button type="button" onClick={() => setSelected([])}>Clear all</button>
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
`ComboboxClear` is exported from `packages/ds/src/index.ts` with the `type` modifier (`type ComboboxClear`) — it is a TypeScript type alias, not a React component value. Rendering it as JSX produces a `React.createElement(undefined, …)` call and throws at runtime.
|
|
370
|
+
|
|
371
|
+
Source: `packages/ds/src/components/combobox.tsx`
|
|
372
|
+
|
|
373
|
+
### Mistake 4 — Passing `onSearch` / `debounceDuration` directly to Combobox
|
|
374
|
+
|
|
375
|
+
Wrong block:
|
|
376
|
+
```tsx
|
|
377
|
+
<Combobox
|
|
378
|
+
multiple
|
|
379
|
+
items={options}
|
|
380
|
+
onSearch={handleSearch}
|
|
381
|
+
debounceDuration={300}
|
|
382
|
+
>
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Correct block:
|
|
386
|
+
```tsx
|
|
387
|
+
// Debounce in the caller; drive filtering through `items`
|
|
388
|
+
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
389
|
+
|
|
390
|
+
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
391
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
392
|
+
debounceRef.current = setTimeout(() => {
|
|
393
|
+
fetchOrFilter(e.target.value).then(setOptions);
|
|
394
|
+
}, 300);
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
<Combobox multiple items={options} ...>
|
|
398
|
+
<ComboboxChipsInput onChange={handleSearch} />
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
`Combobox` has no `onSearch` or `debounceDuration` prop. Search and debounce are caller-owned: attach `onChange` to `ComboboxChipsInput`, debounce the callback, and update `items` via state.
|
|
402
|
+
|
|
403
|
+
Source: `packages/ds/src/components/combobox.tsx`
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## 5. Inside a form — use `FormMultiComboboxField`, not a raw `Combobox`
|
|
408
|
+
|
|
409
|
+
When the multi-select is a **field of an `@aircall/blocks` `useForm`** (the common case
|
|
410
|
+
when migrating a `MultiInlineSearchSelect` that lived in a `FormWizard` / `FormField`),
|
|
411
|
+
don't wire a raw `Combobox` + `useState`. Use `FormMultiComboboxField` from
|
|
412
|
+
`@aircall/blocks` — it owns the value, validation, and error wiring (see
|
|
413
|
+
`aircall-blocks/migrate-dashboard/form-wizard`). It changes three things vs §3:
|
|
414
|
+
|
|
415
|
+
- **The bound field is `string[]` (ids), not option objects.** `FormMultiComboboxField`
|
|
416
|
+
binds `name: DeepKeysOfType<values, string[]>` and its render-prop control bundle gives
|
|
417
|
+
`comboboxProps = { value: string[]; onValueChange: (string[]) => void }`. Spread it onto
|
|
418
|
+
`Combobox` and pass `items={ids}` (string ids) — the inverse of §3's option-object model.
|
|
419
|
+
- **Keep an id→label cache** (`useRef(new Map())`) populated from each server-search page,
|
|
420
|
+
because a selected id may fall outside the current page; chips/options read
|
|
421
|
+
`cache.get(id)?.label ?? id`.
|
|
422
|
+
- **Reshape rich domain types to `string[]` and reconstruct at the submit boundary.** A
|
|
423
|
+
field like `assignees: {id,type}[]` or `callerIdNumbers: {id,phoneNumber}[]` can't bind.
|
|
424
|
+
Store `xIds: string[]` and rebuild the payload in your create/update mapper — either
|
|
425
|
+
collapse to a constant (`assignedUserIds: ids.map(Number)`, all teammates) or reconstruct
|
|
426
|
+
a schema-required richer shape from a lookup map (`ids → {ID, phoneNumber}` from the
|
|
427
|
+
eligibility query, read cache-first at the page level). An already-`string[]` field (e.g.
|
|
428
|
+
`outcomeIds`) needs no reshape.
|
|
429
|
+
|
|
430
|
+
```tsx
|
|
431
|
+
import { FormMultiComboboxField } from '@aircall/blocks';
|
|
432
|
+
import {
|
|
433
|
+
Combobox, ComboboxChip, ComboboxChips, ComboboxChipsInput,
|
|
434
|
+
ComboboxContent, ComboboxEmpty, ComboboxItem, ComboboxList,
|
|
435
|
+
ComboboxValue, useComboboxAnchor,
|
|
436
|
+
} from '@aircall/ds';
|
|
437
|
+
|
|
438
|
+
const [search, setSearch] = useState('');
|
|
439
|
+
const anchor = useComboboxAnchor();
|
|
440
|
+
const { data } = useTeamSearch(search); // server search
|
|
441
|
+
const ids = useMemo(() => (data?.items ?? []).map(t => t.ID), [data]);
|
|
442
|
+
const labels = useRef(new Map<string, string>());
|
|
443
|
+
for (const t of data?.items ?? []) labels.current.set(t.ID, t.name);
|
|
444
|
+
|
|
445
|
+
<FormMultiComboboxField form={form} name="assignedTeamIds" label="Teams">
|
|
446
|
+
{(_field, { comboboxProps, comboboxInputProps }) => (
|
|
447
|
+
<Combobox multiple items={ids} filter={null}
|
|
448
|
+
inputValue={search} onInputValueChange={setSearch} {...comboboxProps}>
|
|
449
|
+
<ComboboxChips ref={anchor}>
|
|
450
|
+
<ComboboxValue>
|
|
451
|
+
{(selected: string[]) => (
|
|
452
|
+
<>
|
|
453
|
+
{selected.map(id => (
|
|
454
|
+
<ComboboxChip key={id}>{labels.current.get(id) ?? id}</ComboboxChip>
|
|
455
|
+
))}
|
|
456
|
+
<ComboboxChipsInput {...comboboxInputProps} placeholder={selected.length ? '' : 'Search teams'} />
|
|
457
|
+
</>
|
|
458
|
+
)}
|
|
459
|
+
</ComboboxValue>
|
|
460
|
+
</ComboboxChips>
|
|
461
|
+
<ComboboxContent anchor={anchor}>
|
|
462
|
+
<ComboboxEmpty>No teams found</ComboboxEmpty>
|
|
463
|
+
<ComboboxList>
|
|
464
|
+
{(id: string) => (
|
|
465
|
+
<ComboboxItem key={id} value={id}>{labels.current.get(id) ?? id}</ComboboxItem>
|
|
466
|
+
)}
|
|
467
|
+
</ComboboxList>
|
|
468
|
+
</ComboboxContent>
|
|
469
|
+
</Combobox>
|
|
470
|
+
)}
|
|
471
|
+
</FormMultiComboboxField>
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
`filter={null}` = manual/server search (else Base UI double-filters the server-filtered list).
|
|
475
|
+
|
|
476
|
+
**More form-bound specifics:**
|
|
477
|
+
- **Coloured chips** (e.g. outcomes): cache `{name, color}`, pass the hex to `ComboboxChip`'s
|
|
478
|
+
`legacyColor?: string` (guard null) — `legacyColor={cache.get(id)?.color ?? undefined}`.
|
|
479
|
+
- **Cap at N:** hide options at the limit (`items={isAtLimit ? [] : ids}`, `isAtLimit` from
|
|
480
|
+
`field.state.value.length` in the render-prop) **and** slice any other write path (inline create-and-select).
|
|
481
|
+
- **Switch-gated:** render under `<form.Subscribe selector={s => s.values.flag}>`; clear on
|
|
482
|
+
disable by wrapping `switchProps.onCheckedChange` → `form.setFieldValue('xIds', [])`.
|
|
483
|
+
|
|
484
|
+
> Testing: opening these comboboxes (and toggling the switch) in jsdom needs the selector guard +
|
|
485
|
+
> hidden-checkbox toggle from `aircall-ds/setup`. Without them the popup crashes on open.
|
|
486
|
+
|
|
487
|
+
Source: `packages/blocks/src/form/form-field.tsx`
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: aircall-blocks/migrate-dashboard/copy-button
|
|
3
|
+
description: >
|
|
4
|
+
Migrate @dashboard/library CopyToClipboardButton and CopyToClipboardText to
|
|
5
|
+
@aircall/blocks CopyButton compound. Load when a file imports
|
|
6
|
+
CopyToClipboardButton or CopyToClipboardText from @dashboard/library.
|
|
7
|
+
type: sub-skill
|
|
8
|
+
library: aircall-blocks
|
|
9
|
+
library_version: "0.6.0"
|
|
10
|
+
requires:
|
|
11
|
+
- aircall-blocks/setup
|
|
12
|
+
- aircall-blocks/migrate-dashboard
|
|
13
|
+
sources:
|
|
14
|
+
- "aircall/hydra:packages/blocks/src/index.ts"
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
This skill builds on aircall-blocks/migrate-dashboard.
|
|
18
|
+
|
|
19
|
+
## 1. Component mapping
|
|
20
|
+
|
|
21
|
+
| `@dashboard/library` | `@aircall/blocks` |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| `CopyToClipboardButton` | `CopyButton` (outline variant, icon + label) |
|
|
24
|
+
| `CopyToClipboardText` | `CopyButton` (ghost variant, inline text + icon) |
|
|
25
|
+
|
|
26
|
+
Both map to the same `CopyButton` compound from `@aircall/blocks`. The visual
|
|
27
|
+
difference (button vs. inline text) is controlled by the `variant` prop.
|
|
28
|
+
|
|
29
|
+
## 2. Verified blocks exports
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
CopyButton, CopyButtonIcon, CopyButtonLabel
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Import from `@aircall/blocks`, not `@aircall/ds`.
|
|
36
|
+
|
|
37
|
+
## 3. Imports
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import { CopyButton, CopyButtonIcon, CopyButtonLabel } from '@aircall/blocks';
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 4. Prop mapping
|
|
44
|
+
|
|
45
|
+
### `CopyToClipboardButton`
|
|
46
|
+
|
|
47
|
+
| `CopyToClipboardButton` prop | `CopyButton` equivalent |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| `content` | `value` prop on `<CopyButton>` |
|
|
50
|
+
| `texts.copy` | `label` on `<CopyButtonLabel>` |
|
|
51
|
+
| `texts.copied` | `copiedLabel` on `<CopyButtonLabel>` |
|
|
52
|
+
| `variant` (button variant) | `variant` on `<CopyButton>` (default: `"outline"`) |
|
|
53
|
+
|
|
54
|
+
### `CopyToClipboardText`
|
|
55
|
+
|
|
56
|
+
| `CopyToClipboardText` prop | `CopyButton` equivalent |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| `children` (the text to display) | text node child inside `<CopyButton>` |
|
|
59
|
+
| `tooltipTexts.copy` | _(tooltip not used — CopyButton has no built-in tooltip)_ |
|
|
60
|
+
| content to copy | `value` prop on `<CopyButton>` |
|
|
61
|
+
| inline text appearance | `variant="ghost"` on `<CopyButton>` |
|
|
62
|
+
|
|
63
|
+
## 5. Before / After examples
|
|
64
|
+
|
|
65
|
+
### 5a. CopyToClipboardButton → CopyButton (outline)
|
|
66
|
+
|
|
67
|
+
**Before:**
|
|
68
|
+
```tsx
|
|
69
|
+
import { CopyToClipboardButton } from '@dashboard/library';
|
|
70
|
+
|
|
71
|
+
<CopyToClipboardButton
|
|
72
|
+
content="+33 6 12 34 56 78"
|
|
73
|
+
texts={{ copy: 'Copy number', copied: 'Copied!' }}
|
|
74
|
+
/>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**After:**
|
|
78
|
+
```tsx
|
|
79
|
+
import { CopyButton, CopyButtonIcon, CopyButtonLabel } from '@aircall/blocks';
|
|
80
|
+
|
|
81
|
+
<CopyButton value="+33 6 12 34 56 78">
|
|
82
|
+
<CopyButtonIcon />
|
|
83
|
+
<CopyButtonLabel label="Copy number" copiedLabel="Copied!" />
|
|
84
|
+
</CopyButton>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 5b. CopyToClipboardText → CopyButton (ghost, inline)
|
|
88
|
+
|
|
89
|
+
**Before:**
|
|
90
|
+
```tsx
|
|
91
|
+
import { CopyToClipboardText } from '@dashboard/library';
|
|
92
|
+
|
|
93
|
+
<CopyToClipboardText tooltipTexts={{ copy: 'Copy', copied: 'Copied' }}>
|
|
94
|
+
+33 6 12 34 56 78
|
|
95
|
+
</CopyToClipboardText>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**After:**
|
|
99
|
+
```tsx
|
|
100
|
+
import { CopyButton, CopyButtonIcon } from '@aircall/blocks';
|
|
101
|
+
|
|
102
|
+
<CopyButton value="+33 6 12 34 56 78" variant="ghost">
|
|
103
|
+
+33 6 12 34 56 78
|
|
104
|
+
<CopyButtonIcon />
|
|
105
|
+
</CopyButton>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
> The `tooltipTexts` prop had no functional role in `CopyToClipboardText` beyond
|
|
109
|
+
> tooltip copy — `CopyButton` has no built-in tooltip, so drop it.
|
|
110
|
+
|
|
111
|
+
## 6. Common mistakes
|
|
112
|
+
|
|
113
|
+
### Mistake 1: Importing from `@aircall/ds`
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
// WRONG — CopyButton lives in @aircall/blocks, not @aircall/ds
|
|
117
|
+
import { CopyButton } from '@aircall/ds';
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
// CORRECT
|
|
122
|
+
import { CopyButton, CopyButtonIcon, CopyButtonLabel } from '@aircall/blocks';
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Mistake 2: Passing text content directly without CopyButtonLabel
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
// WRONG — plain text as children won't update to "Copied!" state
|
|
129
|
+
<CopyButton value="…">Copy number</CopyButton>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
// CORRECT — CopyButtonLabel handles the copy/copied toggle
|
|
134
|
+
<CopyButton value="…">
|
|
135
|
+
<CopyButtonIcon />
|
|
136
|
+
<CopyButtonLabel label="Copy number" copiedLabel="Copied!" />
|
|
137
|
+
</CopyButton>
|
|
138
|
+
```
|