@handled-ai/design-system 0.18.10 → 0.18.12
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/components/account-contacts-popover.d.ts +5 -1
- package/dist/components/account-contacts-popover.js +25 -4
- package/dist/components/account-contacts-popover.js.map +1 -1
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/data-table-condition-filter.d.ts +15 -3
- package/dist/components/data-table-condition-filter.js +199 -52
- package/dist/components/data-table-condition-filter.js.map +1 -1
- package/dist/components/data-table-filter.js +7 -8
- package/dist/components/data-table-filter.js.map +1 -1
- package/dist/components/entity-panel.d.ts +2 -1
- package/dist/components/entity-panel.js +52 -45
- package/dist/components/entity-panel.js.map +1 -1
- package/dist/components/pill.d.ts +1 -1
- package/dist/components/score-why-chips.d.ts +1 -1
- package/dist/components/signal-priority-popover.d.ts +1 -1
- package/dist/components/signal-priority-popover.js +4 -4
- package/dist/components/signal-priority-popover.js.map +1 -1
- package/dist/components/tabs.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/prototype/index.d.ts +1 -1
- package/dist/prototype/prototype-accounts-view.d.ts +1 -1
- package/dist/prototype/prototype-admin-view.d.ts +1 -1
- package/dist/prototype/prototype-config.d.ts +1 -1
- package/dist/prototype/prototype-inbox-view.d.ts +5 -3
- package/dist/prototype/prototype-inbox-view.js +11 -5
- package/dist/prototype/prototype-inbox-view.js.map +1 -1
- package/dist/prototype/prototype-insights-view.d.ts +1 -1
- package/dist/prototype/prototype-shell.d.ts +1 -1
- package/dist/{signal-priority-popover-BT6CPYNs.d.ts → signal-priority-popover-BEDoPsNE.d.ts} +6 -0
- package/package.json +1 -2
- package/src/components/__tests__/account-contacts-popover.test.tsx +79 -0
- package/src/components/__tests__/data-table-condition-filter.test.tsx +239 -7
- package/src/components/__tests__/entity-panel-header.test.tsx +44 -0
- package/src/components/__tests__/signal-priority-popover.test.tsx +30 -0
- package/src/components/account-contacts-popover.tsx +29 -1
- package/src/components/data-table-condition-filter.tsx +278 -68
- package/src/components/data-table-filter.tsx +6 -9
- package/src/components/entity-panel.tsx +56 -40
- package/src/components/signal-priority-popover.tsx +15 -4
- package/src/prototype/__tests__/detail-view-title-slots.test.tsx +15 -0
- package/src/prototype/prototype-config.ts +2 -0
- package/src/prototype/prototype-inbox-view.tsx +17 -5
|
@@ -103,6 +103,23 @@ const dateField: ConditionFieldDef = {
|
|
|
103
103
|
type: "date",
|
|
104
104
|
};
|
|
105
105
|
|
|
106
|
+
const selectField: ConditionFieldDef = {
|
|
107
|
+
id: "stage",
|
|
108
|
+
label: "Stage",
|
|
109
|
+
type: "select",
|
|
110
|
+
options: [
|
|
111
|
+
{ label: "Qualified", value: "qualified" },
|
|
112
|
+
{ label: "Disqualified", value: "disqualified" },
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const multiSelectField: ConditionFieldDef = {
|
|
117
|
+
id: "industry",
|
|
118
|
+
label: "Industry",
|
|
119
|
+
type: "multi_select",
|
|
120
|
+
options: ["Finance", { label: "Healthcare", value: "healthcare" }, "Education"],
|
|
121
|
+
};
|
|
122
|
+
|
|
106
123
|
const allFields: ConditionFieldDef[] = [
|
|
107
124
|
textField,
|
|
108
125
|
numberField,
|
|
@@ -110,6 +127,12 @@ const allFields: ConditionFieldDef[] = [
|
|
|
110
127
|
dateField,
|
|
111
128
|
];
|
|
112
129
|
|
|
130
|
+
const optionFields: ConditionFieldDef[] = [
|
|
131
|
+
textField,
|
|
132
|
+
selectField,
|
|
133
|
+
multiSelectField,
|
|
134
|
+
];
|
|
135
|
+
|
|
113
136
|
describe("DataTableConditionFilter", () => {
|
|
114
137
|
let onConditionsChange: ReturnType<typeof vi.fn<(conditions: ConditionFilterValue[]) => void>>;
|
|
115
138
|
|
|
@@ -117,6 +140,35 @@ describe("DataTableConditionFilter", () => {
|
|
|
117
140
|
onConditionsChange = vi.fn<(conditions: ConditionFilterValue[]) => void>();
|
|
118
141
|
});
|
|
119
142
|
|
|
143
|
+
const renderOptionFilter = () => {
|
|
144
|
+
const result = render(
|
|
145
|
+
<DataTableConditionFilter
|
|
146
|
+
fields={optionFields}
|
|
147
|
+
conditions={[]}
|
|
148
|
+
onConditionsChange={onConditionsChange}
|
|
149
|
+
/>,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
fireEvent.click(screen.getByText("Add filter"));
|
|
153
|
+
return result;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const chooseField = (container: HTMLElement, label: string) => {
|
|
157
|
+
const fieldOption = Array.from(container.querySelectorAll('[data-slot="select-item"]')).find(
|
|
158
|
+
(opt) => opt.textContent?.includes(label),
|
|
159
|
+
);
|
|
160
|
+
expect(fieldOption).not.toBeUndefined();
|
|
161
|
+
fireEvent.click(fieldOption!);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const expectFieldTriggerHasNoFieldTypeIcon = (container: HTMLElement) => {
|
|
165
|
+
const fieldTrigger = container.querySelector('[data-slot="condition-row"] [data-slot="select-trigger"]');
|
|
166
|
+
expect(fieldTrigger).not.toBeNull();
|
|
167
|
+
expect(fieldTrigger?.querySelector("svg.lucide-ellipsis")).toBeNull();
|
|
168
|
+
expect(fieldTrigger?.querySelector("svg.lucide-list-filter")).toBeNull();
|
|
169
|
+
expect(fieldTrigger?.querySelector("svg.lucide-list-checks")).toBeNull();
|
|
170
|
+
};
|
|
171
|
+
|
|
120
172
|
it("renders a polished empty panel with Add filter, disabled Add filter group, and quiet Clear filters", () => {
|
|
121
173
|
const { container } = render(
|
|
122
174
|
<DataTableConditionFilter
|
|
@@ -151,7 +203,6 @@ describe("DataTableConditionFilter", () => {
|
|
|
151
203
|
expect(onConditionsChange).toHaveBeenCalledWith([]);
|
|
152
204
|
});
|
|
153
205
|
|
|
154
|
-
|
|
155
206
|
it("preserves an in-progress draft when fields are recreated with the same definition", () => {
|
|
156
207
|
const { rerender } = render(
|
|
157
208
|
<DataTableConditionFilter
|
|
@@ -197,6 +248,182 @@ describe("DataTableConditionFilter", () => {
|
|
|
197
248
|
expect(committed?.[0]).toMatchObject({ field: "name", operator: "eq", value: "Acme" });
|
|
198
249
|
});
|
|
199
250
|
|
|
251
|
+
it("renders select options and commits an eq condition", () => {
|
|
252
|
+
const { container } = renderOptionFilter();
|
|
253
|
+
chooseField(container, "Stage");
|
|
254
|
+
|
|
255
|
+
expectFieldTriggerHasNoFieldTypeIcon(container);
|
|
256
|
+
|
|
257
|
+
expect(getOperators(selectField)).toEqual(["eq", "neq", "is_null", "is_not_null"]);
|
|
258
|
+
fireEvent.click(screen.getByText("Qualified"));
|
|
259
|
+
fireEvent.click(screen.getByText("Apply"));
|
|
260
|
+
|
|
261
|
+
const committed = onConditionsChange.mock.calls.at(-1)?.[0];
|
|
262
|
+
expect(committed).toHaveLength(1);
|
|
263
|
+
expect(committed?.[0]).toMatchObject({
|
|
264
|
+
field: "stage",
|
|
265
|
+
operator: "eq",
|
|
266
|
+
value: "qualified",
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("filters searchable select options before committing a selected value", () => {
|
|
271
|
+
const searchableOwnerField: ConditionFieldDef = {
|
|
272
|
+
id: "owner",
|
|
273
|
+
label: "Owner",
|
|
274
|
+
type: "select",
|
|
275
|
+
searchable: true,
|
|
276
|
+
options: [
|
|
277
|
+
{ label: "Alice Adams", value: "alice@example.com" },
|
|
278
|
+
{ label: "Bob Brown", value: "bob@example.com" },
|
|
279
|
+
],
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
render(
|
|
283
|
+
<DataTableConditionFilter
|
|
284
|
+
fields={[searchableOwnerField]}
|
|
285
|
+
conditions={[]}
|
|
286
|
+
onConditionsChange={onConditionsChange}
|
|
287
|
+
/>,
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
fireEvent.click(screen.getByText("Add filter"));
|
|
291
|
+
fireEvent.change(screen.getByPlaceholderText("Search options..."), {
|
|
292
|
+
target: { value: "bob" },
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
expect(screen.queryByText("Alice Adams")).toBeNull();
|
|
296
|
+
fireEvent.click(screen.getByText("Bob Brown"));
|
|
297
|
+
fireEvent.click(screen.getByText("Apply"));
|
|
298
|
+
|
|
299
|
+
const committed = onConditionsChange.mock.calls.at(-1)?.[0];
|
|
300
|
+
expect(committed).toHaveLength(1);
|
|
301
|
+
expect(committed?.[0]).toMatchObject({
|
|
302
|
+
field: "owner",
|
|
303
|
+
operator: "eq",
|
|
304
|
+
value: "bob@example.com",
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("uses DataTableFilter threshold semantics for option search", () => {
|
|
309
|
+
const thresholdField: ConditionFieldDef = {
|
|
310
|
+
id: "threshold",
|
|
311
|
+
label: "Threshold",
|
|
312
|
+
type: "select",
|
|
313
|
+
searchable: { threshold: 2 },
|
|
314
|
+
options: [
|
|
315
|
+
{ label: "One", value: "one" },
|
|
316
|
+
{ label: "Two", value: "two" },
|
|
317
|
+
],
|
|
318
|
+
};
|
|
319
|
+
const aboveThresholdField: ConditionFieldDef = {
|
|
320
|
+
...thresholdField,
|
|
321
|
+
id: "above_threshold",
|
|
322
|
+
options: [
|
|
323
|
+
{ label: "One", value: "one" },
|
|
324
|
+
{ label: "Two", value: "two" },
|
|
325
|
+
{ label: "Three", value: "three" },
|
|
326
|
+
],
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const { rerender } = render(
|
|
330
|
+
<DataTableConditionFilter
|
|
331
|
+
fields={[thresholdField]}
|
|
332
|
+
conditions={[]}
|
|
333
|
+
onConditionsChange={onConditionsChange}
|
|
334
|
+
/>,
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
fireEvent.click(screen.getByText("Add filter"));
|
|
338
|
+
expect(screen.queryByPlaceholderText("Search options...")).toBeNull();
|
|
339
|
+
|
|
340
|
+
rerender(
|
|
341
|
+
<DataTableConditionFilter
|
|
342
|
+
fields={[aboveThresholdField]}
|
|
343
|
+
conditions={[]}
|
|
344
|
+
onConditionsChange={onConditionsChange}
|
|
345
|
+
/>,
|
|
346
|
+
);
|
|
347
|
+
expect(screen.getByPlaceholderText("Search options...")).toBeDefined();
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("does not apply a stale search query after switching to a non-searchable select field", () => {
|
|
351
|
+
const searchableOwnerField: ConditionFieldDef = {
|
|
352
|
+
id: "owner",
|
|
353
|
+
label: "Owner",
|
|
354
|
+
type: "select",
|
|
355
|
+
searchable: true,
|
|
356
|
+
options: [
|
|
357
|
+
{ label: "Alice Adams", value: "alice@example.com" },
|
|
358
|
+
{ label: "Bob Brown", value: "bob@example.com" },
|
|
359
|
+
],
|
|
360
|
+
};
|
|
361
|
+
const statusField: ConditionFieldDef = {
|
|
362
|
+
id: "status",
|
|
363
|
+
label: "Status",
|
|
364
|
+
type: "select",
|
|
365
|
+
searchable: false,
|
|
366
|
+
options: [
|
|
367
|
+
{ label: "Active", value: "active" },
|
|
368
|
+
{ label: "Inactive", value: "inactive" },
|
|
369
|
+
],
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const { container } = render(
|
|
373
|
+
<DataTableConditionFilter
|
|
374
|
+
fields={[searchableOwnerField, statusField]}
|
|
375
|
+
conditions={[]}
|
|
376
|
+
onConditionsChange={onConditionsChange}
|
|
377
|
+
/>,
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
fireEvent.click(screen.getByText("Add filter"));
|
|
381
|
+
fireEvent.change(screen.getByPlaceholderText("Search options..."), {
|
|
382
|
+
target: { value: "bob" },
|
|
383
|
+
});
|
|
384
|
+
chooseField(container, "Status");
|
|
385
|
+
|
|
386
|
+
expect(screen.queryByPlaceholderText("Search options...")).toBeNull();
|
|
387
|
+
expect(screen.getByText("Active")).toBeDefined();
|
|
388
|
+
expect(screen.getByText("Inactive")).toBeDefined();
|
|
389
|
+
expect(screen.queryByText("No options")).toBeNull();
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("renders multi-select checkboxes, toggles multiple options, and commits an in condition", () => {
|
|
393
|
+
const { container } = renderOptionFilter();
|
|
394
|
+
chooseField(container, "Industry");
|
|
395
|
+
|
|
396
|
+
expectFieldTriggerHasNoFieldTypeIcon(container);
|
|
397
|
+
|
|
398
|
+
expect(getOperators(multiSelectField)).toEqual(["in", "is_null", "is_not_null"]);
|
|
399
|
+
|
|
400
|
+
const financeCheckbox = screen.getByRole("checkbox", { name: "Finance" });
|
|
401
|
+
const healthcareCheckbox = screen.getByRole("checkbox", { name: "Healthcare" });
|
|
402
|
+
fireEvent.click(financeCheckbox);
|
|
403
|
+
fireEvent.click(healthcareCheckbox);
|
|
404
|
+
|
|
405
|
+
expect(financeCheckbox.getAttribute("aria-checked")).toBe("true");
|
|
406
|
+
expect(healthcareCheckbox.getAttribute("aria-checked")).toBe("true");
|
|
407
|
+
|
|
408
|
+
fireEvent.click(screen.getByText("Apply"));
|
|
409
|
+
|
|
410
|
+
const committed = onConditionsChange.mock.calls.at(-1)?.[0];
|
|
411
|
+
expect(committed).toHaveLength(1);
|
|
412
|
+
expect(committed?.[0]).toMatchObject({
|
|
413
|
+
field: "industry",
|
|
414
|
+
operator: "in",
|
|
415
|
+
value: ["Finance", "healthcare"],
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it("omits incomplete multi-select conditions until at least one option is selected", () => {
|
|
420
|
+
const { container } = renderOptionFilter();
|
|
421
|
+
chooseField(container, "Industry");
|
|
422
|
+
fireEvent.click(screen.getByText("Apply"));
|
|
423
|
+
|
|
424
|
+
expect(onConditionsChange.mock.calls.at(-1)?.[0]).toEqual([]);
|
|
425
|
+
});
|
|
426
|
+
|
|
200
427
|
it("Enter in the value field commits the current draft row", () => {
|
|
201
428
|
render(
|
|
202
429
|
<DataTableConditionFilter
|
|
@@ -380,9 +607,13 @@ describe("DataTableConditionFilter", () => {
|
|
|
380
607
|
expect(screen.getAllByText("And")).toHaveLength(2);
|
|
381
608
|
});
|
|
382
609
|
|
|
383
|
-
it("operator options
|
|
610
|
+
it("operator options respect the selected field definition", () => {
|
|
384
611
|
expect(getOperators(textField)).toEqual(["eq", "neq", "is_null", "is_not_null"]);
|
|
385
|
-
expect(getOperators({ ...textField, operators: ["eq", "in", "is_null"] })).toEqual([
|
|
612
|
+
expect(getOperators({ ...textField, operators: ["eq", "in", "is_null"] })).toEqual([
|
|
613
|
+
"eq",
|
|
614
|
+
"in",
|
|
615
|
+
"is_null",
|
|
616
|
+
]);
|
|
386
617
|
|
|
387
618
|
const { container } = render(
|
|
388
619
|
<DataTableConditionFilter
|
|
@@ -392,9 +623,10 @@ describe("DataTableConditionFilter", () => {
|
|
|
392
623
|
/>,
|
|
393
624
|
);
|
|
394
625
|
|
|
395
|
-
const optionTexts = Array.from(container.querySelectorAll('[data-slot="select-item"]'))
|
|
396
|
-
|
|
397
|
-
|
|
626
|
+
const optionTexts = Array.from(container.querySelectorAll('[data-slot="select-item"]')).map(
|
|
627
|
+
(el) => el.textContent,
|
|
628
|
+
);
|
|
629
|
+
expect(optionTexts).not.toContain("is any of");
|
|
398
630
|
expect(optionTexts).toContain("is empty");
|
|
399
631
|
});
|
|
400
632
|
|
|
@@ -413,7 +645,7 @@ describe("DataTableConditionFilter", () => {
|
|
|
413
645
|
expect(optionTexts).toContain("≥");
|
|
414
646
|
expect(optionTexts).toContain("<");
|
|
415
647
|
expect(optionTexts).toContain("≤");
|
|
416
|
-
expect(optionTexts).not.toContain("
|
|
648
|
+
expect(optionTexts).not.toContain("is any of");
|
|
417
649
|
});
|
|
418
650
|
|
|
419
651
|
it("generateConditionId returns unique values", () => {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest"
|
|
2
|
+
import React from "react"
|
|
3
|
+
import { render, screen } from "@testing-library/react"
|
|
4
|
+
|
|
5
|
+
import { EntityPanel, EntityPanelHeader } from "../entity-panel"
|
|
6
|
+
|
|
7
|
+
function renderHeader(node: React.ReactNode) {
|
|
8
|
+
return render(
|
|
9
|
+
<EntityPanel isOpen onClose={vi.fn()}>
|
|
10
|
+
{node}
|
|
11
|
+
</EntityPanel>,
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("EntityPanelHeader", () => {
|
|
16
|
+
it("preserves headerAction and panel controls", () => {
|
|
17
|
+
renderHeader(
|
|
18
|
+
<EntityPanelHeader
|
|
19
|
+
title="Acme Corp"
|
|
20
|
+
headerAction={<button type="button">Mercury Admin</button>}
|
|
21
|
+
/>,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
expect(screen.getByText("Acme Corp")).toBeTruthy()
|
|
25
|
+
expect(screen.getByRole("button", { name: "Mercury Admin" })).toBeTruthy()
|
|
26
|
+
expect(screen.getByTitle("Copy Link")).toBeTruthy()
|
|
27
|
+
expect(screen.getByTitle("Wide")).toBeTruthy()
|
|
28
|
+
expect(screen.getByTitle("Close")).toBeTruthy()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("renders subtitle and secondary action on the secondary row", () => {
|
|
32
|
+
renderHeader(
|
|
33
|
+
<EntityPanelHeader
|
|
34
|
+
title="Acme Corp"
|
|
35
|
+
subtitle="Preferred account · Treasury"
|
|
36
|
+
headerSecondaryAction={<button type="button">Quick action</button>}
|
|
37
|
+
/>,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
expect(screen.getByText("Preferred account · Treasury")).toBeTruthy()
|
|
41
|
+
expect(screen.getByRole("button", { name: "Quick action" })).toBeTruthy()
|
|
42
|
+
expect(screen.getByTitle("Copy Link")).toBeTruthy()
|
|
43
|
+
})
|
|
44
|
+
})
|
|
@@ -157,6 +157,36 @@ describe("SignalPriorityPopover", () => {
|
|
|
157
157
|
expect(neutralRow.textContent).toContain("50")
|
|
158
158
|
})
|
|
159
159
|
|
|
160
|
+
|
|
161
|
+
it("keeps default direction and score labels unchanged", () => {
|
|
162
|
+
render(<SignalPriorityPopover {...defaultProps} />)
|
|
163
|
+
fireEvent.click(screen.getByTestId("priority-popover-trigger"))
|
|
164
|
+
|
|
165
|
+
const row = screen.getByTestId("factor-row-test_severity")
|
|
166
|
+
expect(row.textContent).toContain("Raises")
|
|
167
|
+
expect(row.textContent).toContain("85")
|
|
168
|
+
expect(row.textContent).toContain("/100")
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it("renders custom direction and display value labels", () => {
|
|
172
|
+
const customFactors: PriorityFactor[] = [
|
|
173
|
+
{
|
|
174
|
+
...mockFactors[0],
|
|
175
|
+
directionLabel: "Raises urgency",
|
|
176
|
+
displayValueLabel: "Open window",
|
|
177
|
+
score: 0,
|
|
178
|
+
},
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
render(<SignalPriorityPopover {...defaultProps} factors={customFactors} />)
|
|
182
|
+
fireEvent.click(screen.getByTestId("priority-popover-trigger"))
|
|
183
|
+
|
|
184
|
+
const row = screen.getByTestId("factor-row-test_severity")
|
|
185
|
+
expect(row.textContent).toContain("Raises urgency")
|
|
186
|
+
expect(row.textContent).toContain("Open window")
|
|
187
|
+
expect(row.textContent).not.toContain("Raises0/100")
|
|
188
|
+
})
|
|
189
|
+
|
|
160
190
|
it("renders Contributing factors section label", () => {
|
|
161
191
|
render(<SignalPriorityPopover {...defaultProps} />)
|
|
162
192
|
fireEvent.click(screen.getByTestId("priority-popover-trigger"))
|
|
@@ -32,6 +32,10 @@ export interface AccountContactsPopoverProps {
|
|
|
32
32
|
onSelectTo?: (contact: SuggestedContact) => void
|
|
33
33
|
onSelectCc?: (contact: SuggestedContact) => void
|
|
34
34
|
onSelectBcc?: (contact: SuggestedContact) => void
|
|
35
|
+
/** Label for the default contact row action. Defaults to "Add" or "Switch" when onSelectSwitch is provided. */
|
|
36
|
+
defaultSelectLabel?: string
|
|
37
|
+
/** Optional replacement-selection callback. When provided, row clicks call this instead of additive onSelect/onSelectTo. */
|
|
38
|
+
onSelectSwitch?: (contact: SuggestedContact) => void
|
|
35
39
|
onViewAll?: () => void
|
|
36
40
|
onOpenRecentActivity?: () => void
|
|
37
41
|
trigger: React.ReactNode
|
|
@@ -44,6 +48,8 @@ export function AccountContactsPopover({
|
|
|
44
48
|
onSelectTo,
|
|
45
49
|
onSelectCc,
|
|
46
50
|
onSelectBcc,
|
|
51
|
+
defaultSelectLabel,
|
|
52
|
+
onSelectSwitch,
|
|
47
53
|
onViewAll,
|
|
48
54
|
onOpenRecentActivity,
|
|
49
55
|
trigger,
|
|
@@ -52,6 +58,15 @@ export function AccountContactsPopover({
|
|
|
52
58
|
const [open, setOpen] = React.useState(false)
|
|
53
59
|
const triggerRef = React.useRef<HTMLDivElement>(null)
|
|
54
60
|
const [popoverStyle, setPopoverStyle] = React.useState<React.CSSProperties>({})
|
|
61
|
+
const resolvedDefaultSelectLabel = defaultSelectLabel ?? (onSelectSwitch ? "Switch" : undefined)
|
|
62
|
+
const handleDefaultSelect = React.useCallback((contact: SuggestedContact) => {
|
|
63
|
+
if (onSelectSwitch) {
|
|
64
|
+
onSelectSwitch(contact)
|
|
65
|
+
} else {
|
|
66
|
+
(onSelectTo ?? onSelect)(contact)
|
|
67
|
+
}
|
|
68
|
+
setOpen(false)
|
|
69
|
+
}, [onSelect, onSelectSwitch, onSelectTo])
|
|
55
70
|
|
|
56
71
|
React.useEffect(() => {
|
|
57
72
|
if (open && triggerRef.current) {
|
|
@@ -84,7 +99,8 @@ export function AccountContactsPopover({
|
|
|
84
99
|
<div
|
|
85
100
|
key={i}
|
|
86
101
|
role="button"
|
|
87
|
-
onClick={() =>
|
|
102
|
+
onClick={() => handleDefaultSelect(c)}
|
|
103
|
+
aria-label={resolvedDefaultSelectLabel ? `${resolvedDefaultSelectLabel} ${c.name}` : undefined}
|
|
88
104
|
className="flex items-center gap-3 w-full px-3 py-2 text-left hover:bg-muted/50 transition-colors cursor-pointer"
|
|
89
105
|
>
|
|
90
106
|
<div className="w-7 h-7 rounded-full bg-muted flex items-center justify-center text-[10px] font-medium text-muted-foreground shrink-0">
|
|
@@ -115,6 +131,18 @@ export function AccountContactsPopover({
|
|
|
115
131
|
)}
|
|
116
132
|
</div>
|
|
117
133
|
<div className="ml-2 flex items-center gap-1.5 shrink-0">
|
|
134
|
+
{resolvedDefaultSelectLabel && (
|
|
135
|
+
<button
|
|
136
|
+
type="button"
|
|
137
|
+
onClick={(e) => {
|
|
138
|
+
e.stopPropagation()
|
|
139
|
+
handleDefaultSelect(c)
|
|
140
|
+
}}
|
|
141
|
+
className="h-6 rounded border border-border bg-background px-1.5 text-[10px] text-muted-foreground hover:text-foreground hover:bg-muted/40"
|
|
142
|
+
>
|
|
143
|
+
{resolvedDefaultSelectLabel}
|
|
144
|
+
</button>
|
|
145
|
+
)}
|
|
118
146
|
{onSelectTo && (
|
|
119
147
|
<button
|
|
120
148
|
type="button"
|