@handled-ai/design-system 0.17.2 → 0.18.1
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/charts/empty-chart-state.d.ts +11 -0
- package/dist/charts/empty-chart-state.js +70 -0
- package/dist/charts/empty-chart-state.js.map +1 -0
- package/dist/charts/index.d.ts +1 -0
- package/dist/charts/index.js +1 -0
- package/dist/charts/index.js.map +1 -1
- package/dist/charts/pipeline-overview.d.ts +2 -1
- package/dist/charts/pipeline-overview.js +29 -1
- package/dist/charts/pipeline-overview.js.map +1 -1
- package/dist/components/actor-byline.d.ts +3 -0
- package/dist/components/actor-byline.js +5 -0
- package/dist/components/actor-byline.js.map +1 -0
- package/dist/components/days-open-cell.d.ts +16 -0
- package/dist/components/days-open-cell.js +73 -0
- package/dist/components/days-open-cell.js.map +1 -0
- package/dist/components/detail-drawer.d.ts +16 -0
- package/dist/components/detail-drawer.js +45 -0
- package/dist/components/detail-drawer.js.map +1 -0
- package/dist/components/insights-filter-bar.d.ts +2 -1
- package/dist/components/insights-filter-bar.js +13 -5
- package/dist/components/insights-filter-bar.js.map +1 -1
- package/dist/components/linked-entity-cell.d.ts +14 -0
- package/dist/components/linked-entity-cell.js +96 -0
- package/dist/components/linked-entity-cell.js.map +1 -0
- package/dist/components/metric-card.d.ts +14 -1
- package/dist/components/metric-card.js +86 -0
- package/dist/components/metric-card.js.map +1 -1
- package/dist/components/performance-metrics-table.d.ts +2 -1
- package/dist/components/performance-metrics-table.js +78 -46
- package/dist/components/performance-metrics-table.js.map +1 -1
- package/dist/components/pill.d.ts +26 -0
- package/dist/components/pill.js +77 -0
- package/dist/components/pill.js.map +1 -0
- package/dist/components/quick-segment.d.ts +13 -0
- package/dist/components/quick-segment.js +96 -0
- package/dist/components/quick-segment.js.map +1 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/__tests__/insights-charts.test.tsx +62 -0
- package/src/charts/empty-chart-state.tsx +44 -0
- package/src/charts/index.ts +1 -0
- package/src/charts/pipeline-overview.tsx +38 -1
- package/src/components/__tests__/insights-primitives.test.tsx +117 -0
- package/src/components/__tests__/performance-metrics-table.test.tsx +54 -0
- package/src/components/__tests__/user-display.test.tsx +75 -0
- package/src/components/actor-byline.tsx +1 -0
- package/src/components/days-open-cell.tsx +50 -0
- package/src/components/detail-drawer.tsx +60 -0
- package/src/components/insights-filter-bar.tsx +13 -4
- package/src/components/linked-entity-cell.tsx +74 -0
- package/src/components/metric-card.tsx +82 -0
- package/src/components/performance-metrics-table.tsx +99 -63
- package/src/components/pill.tsx +67 -0
- package/src/components/quick-segment.tsx +68 -0
- package/src/index.ts +5 -0
- package/src/lib/__tests__/user-display.test.ts +53 -11
|
@@ -52,6 +52,7 @@ interface PerformanceMetricsTableProps {
|
|
|
52
52
|
title?: string
|
|
53
53
|
entityColumnLabel?: string
|
|
54
54
|
primaryMetricColumnLabel?: string
|
|
55
|
+
primaryMetricDisplayMode?: "progress" | "value"
|
|
55
56
|
rateColumnLabel?: string
|
|
56
57
|
metricOneColumnLabel?: string
|
|
57
58
|
metricTwoColumnLabel?: string
|
|
@@ -179,9 +180,48 @@ function getProgressStatus(value: number, target: number) {
|
|
|
179
180
|
}
|
|
180
181
|
}
|
|
181
182
|
|
|
183
|
+
function PrimaryMetricCell({
|
|
184
|
+
row,
|
|
185
|
+
displayMode,
|
|
186
|
+
}: {
|
|
187
|
+
row: PerformanceMetricsTableRow
|
|
188
|
+
displayMode: "progress" | "value"
|
|
189
|
+
}) {
|
|
190
|
+
if (displayMode === "value") {
|
|
191
|
+
return (
|
|
192
|
+
<div className="text-sm font-bold text-foreground">
|
|
193
|
+
{row.primaryValue}
|
|
194
|
+
</div>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const percentage = (row.primaryValue / row.primaryTarget) * 100
|
|
199
|
+
const progress = getProgressStatus(row.primaryValue, row.primaryTarget)
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<div className="flex items-center gap-2">
|
|
203
|
+
<span className="shrink-0">{progress.icon}</span>
|
|
204
|
+
<div className="w-full max-w-[180px]">
|
|
205
|
+
<div className="mb-1 text-sm font-bold text-foreground">
|
|
206
|
+
{row.primaryValue}/{row.primaryTarget}
|
|
207
|
+
</div>
|
|
208
|
+
<div className="h-1.5 w-full overflow-hidden rounded-full bg-muted">
|
|
209
|
+
<div
|
|
210
|
+
className={cn("h-full rounded-full", progress.color)}
|
|
211
|
+
style={{ width: `${Math.min(100, percentage)}%` }}
|
|
212
|
+
/>
|
|
213
|
+
</div>
|
|
214
|
+
<div className={cn("mt-1 text-xs font-medium", progress.textColor)}>
|
|
215
|
+
{Math.round(percentage)}%
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
182
222
|
function sortRows(
|
|
183
223
|
rows: PerformanceMetricsTableRow[],
|
|
184
|
-
sortId: PerformanceMetricsTableSortOption["id"]
|
|
224
|
+
sortId: PerformanceMetricsTableSortOption["id"],
|
|
185
225
|
) {
|
|
186
226
|
const copy = [...rows]
|
|
187
227
|
switch (sortId) {
|
|
@@ -201,23 +241,28 @@ export function PerformanceMetricsTable({
|
|
|
201
241
|
title = "Performance Table",
|
|
202
242
|
entityColumnLabel = "Entity",
|
|
203
243
|
primaryMetricColumnLabel = "Primary Goal",
|
|
244
|
+
primaryMetricDisplayMode = "progress",
|
|
204
245
|
rateColumnLabel = "Rate",
|
|
205
246
|
metricOneColumnLabel = "Metric One",
|
|
206
247
|
metricTwoColumnLabel = "Metric Two",
|
|
207
248
|
metricThreeColumnLabel = "Metric Three",
|
|
208
249
|
metricFourColumnLabel = "Metric Four",
|
|
209
250
|
viewOptions = ["By Entity"],
|
|
210
|
-
roleOptions = [
|
|
251
|
+
roleOptions = [
|
|
252
|
+
"All",
|
|
253
|
+
"Senior Coordinator",
|
|
254
|
+
"Coordinator",
|
|
255
|
+
"Junior Coordinator",
|
|
256
|
+
],
|
|
211
257
|
sortOptions = DEFAULT_SORT_OPTIONS,
|
|
212
258
|
rows = DEFAULT_ROWS,
|
|
213
259
|
pageSize = 6,
|
|
214
260
|
searchPlaceholder = "Search rows...",
|
|
215
261
|
}: PerformanceMetricsTableProps) {
|
|
216
262
|
const [view, setView] = React.useState(viewOptions[0] ?? "By Entity")
|
|
217
|
-
const [sortId, setSortId] =
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
)
|
|
263
|
+
const [sortId, setSortId] = React.useState<
|
|
264
|
+
PerformanceMetricsTableSortOption["id"]
|
|
265
|
+
>(sortOptions[0]?.id ?? "primary-desc")
|
|
221
266
|
const [roleFilter, setRoleFilter] = React.useState(roleOptions[0] ?? "All")
|
|
222
267
|
const [search, setSearch] = React.useState("")
|
|
223
268
|
const [page, setPage] = React.useState(1)
|
|
@@ -237,7 +282,7 @@ export function PerformanceMetricsTable({
|
|
|
237
282
|
|
|
238
283
|
const sortedRows = React.useMemo(
|
|
239
284
|
() => sortRows(filteredRows, sortId),
|
|
240
|
-
[filteredRows, sortId]
|
|
285
|
+
[filteredRows, sortId],
|
|
241
286
|
)
|
|
242
287
|
|
|
243
288
|
const pageCount = Math.max(1, Math.ceil(sortedRows.length / pageSize))
|
|
@@ -285,7 +330,10 @@ export function PerformanceMetricsTable({
|
|
|
285
330
|
</DropdownMenuTrigger>
|
|
286
331
|
<DropdownMenuContent align="end" className="w-56">
|
|
287
332
|
{sortOptions.map((option) => (
|
|
288
|
-
<DropdownMenuItem
|
|
333
|
+
<DropdownMenuItem
|
|
334
|
+
key={option.id}
|
|
335
|
+
onClick={() => setSortId(option.id)}
|
|
336
|
+
>
|
|
289
337
|
{option.label}
|
|
290
338
|
</DropdownMenuItem>
|
|
291
339
|
))}
|
|
@@ -301,7 +349,10 @@ export function PerformanceMetricsTable({
|
|
|
301
349
|
</DropdownMenuTrigger>
|
|
302
350
|
<DropdownMenuContent align="end">
|
|
303
351
|
{roleOptions.map((option) => (
|
|
304
|
-
<DropdownMenuItem
|
|
352
|
+
<DropdownMenuItem
|
|
353
|
+
key={option}
|
|
354
|
+
onClick={() => setRoleFilter(option)}
|
|
355
|
+
>
|
|
305
356
|
{option}
|
|
306
357
|
</DropdownMenuItem>
|
|
307
358
|
))}
|
|
@@ -346,61 +397,45 @@ export function PerformanceMetricsTable({
|
|
|
346
397
|
</TableRow>
|
|
347
398
|
</TableHeader>
|
|
348
399
|
<TableBody>
|
|
349
|
-
{paginatedRows.map((row) =>
|
|
350
|
-
|
|
351
|
-
|
|
400
|
+
{paginatedRows.map((row) => (
|
|
401
|
+
<TableRow key={row.id} className="hover:bg-muted/30">
|
|
402
|
+
<TableCell className="px-4 py-3">
|
|
403
|
+
<div className="flex items-center gap-3">
|
|
404
|
+
<Avatar className="h-8 w-8 border border-border">
|
|
405
|
+
<AvatarFallback className="bg-emerald-100 text-[11px] font-medium text-emerald-700">
|
|
406
|
+
{row.avatarFallback}
|
|
407
|
+
</AvatarFallback>
|
|
408
|
+
</Avatar>
|
|
409
|
+
<span className="text-sm font-medium text-foreground">
|
|
410
|
+
{row.label}
|
|
411
|
+
</span>
|
|
412
|
+
</div>
|
|
413
|
+
</TableCell>
|
|
352
414
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
{row.avatarFallback}
|
|
360
|
-
</AvatarFallback>
|
|
361
|
-
</Avatar>
|
|
362
|
-
<span className="text-sm font-medium text-foreground">{row.label}</span>
|
|
363
|
-
</div>
|
|
364
|
-
</TableCell>
|
|
415
|
+
<TableCell className="px-4 py-3">
|
|
416
|
+
<PrimaryMetricCell
|
|
417
|
+
row={row}
|
|
418
|
+
displayMode={primaryMetricDisplayMode}
|
|
419
|
+
/>
|
|
420
|
+
</TableCell>
|
|
365
421
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
</div>
|
|
384
|
-
</TableCell>
|
|
385
|
-
|
|
386
|
-
<TableCell className="px-4 py-3 text-right text-sm font-semibold text-emerald-600">
|
|
387
|
-
{row.ratePercent}%
|
|
388
|
-
</TableCell>
|
|
389
|
-
<TableCell className="px-4 py-3 text-right text-sm font-medium text-foreground">
|
|
390
|
-
{row.metricOne}
|
|
391
|
-
</TableCell>
|
|
392
|
-
<TableCell className="px-4 py-3 text-right text-sm text-muted-foreground">
|
|
393
|
-
{row.metricTwo}
|
|
394
|
-
</TableCell>
|
|
395
|
-
<TableCell className="px-4 py-3 text-right text-sm font-medium text-foreground">
|
|
396
|
-
{row.metricThree}
|
|
397
|
-
</TableCell>
|
|
398
|
-
<TableCell className="px-4 py-3 text-right text-sm font-medium text-foreground">
|
|
399
|
-
{row.metricFour}
|
|
400
|
-
</TableCell>
|
|
401
|
-
</TableRow>
|
|
402
|
-
)
|
|
403
|
-
})}
|
|
422
|
+
<TableCell className="px-4 py-3 text-right text-sm font-semibold text-emerald-600">
|
|
423
|
+
{row.ratePercent}%
|
|
424
|
+
</TableCell>
|
|
425
|
+
<TableCell className="px-4 py-3 text-right text-sm font-medium text-foreground">
|
|
426
|
+
{row.metricOne}
|
|
427
|
+
</TableCell>
|
|
428
|
+
<TableCell className="px-4 py-3 text-right text-sm text-muted-foreground">
|
|
429
|
+
{row.metricTwo}
|
|
430
|
+
</TableCell>
|
|
431
|
+
<TableCell className="px-4 py-3 text-right text-sm font-medium text-foreground">
|
|
432
|
+
{row.metricThree}
|
|
433
|
+
</TableCell>
|
|
434
|
+
<TableCell className="px-4 py-3 text-right text-sm font-medium text-foreground">
|
|
435
|
+
{row.metricFour}
|
|
436
|
+
</TableCell>
|
|
437
|
+
</TableRow>
|
|
438
|
+
))}
|
|
404
439
|
</TableBody>
|
|
405
440
|
</Table>
|
|
406
441
|
</div>
|
|
@@ -410,7 +445,8 @@ export function PerformanceMetricsTable({
|
|
|
410
445
|
<div className="flex items-center justify-between border-t border-border bg-muted/20 px-4 py-3">
|
|
411
446
|
<span className="text-xs text-muted-foreground">
|
|
412
447
|
Showing {sortedRows.length === 0 ? 0 : start + 1} to{" "}
|
|
413
|
-
{Math.min(start + pageSize, sortedRows.length)} of {sortedRows.length}
|
|
448
|
+
{Math.min(start + pageSize, sortedRows.length)} of {sortedRows.length}{" "}
|
|
449
|
+
rows
|
|
414
450
|
</span>
|
|
415
451
|
<div className="flex items-center gap-2">
|
|
416
452
|
<Button
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
|
|
4
|
+
import { cn } from "../lib/utils"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Insights-friendly pill convenience wrappers.
|
|
8
|
+
*
|
|
9
|
+
* Pill and StatusPill are small, rounded wrappers for dense Insights surfaces
|
|
10
|
+
* such as tables, KPI strips, and filter summaries. They intentionally wrap the
|
|
11
|
+
* existing Badge/StatusBadge visual language and are not a replacement for all
|
|
12
|
+
* Badge usage across the design system.
|
|
13
|
+
*/
|
|
14
|
+
export type PillStatus = "success" | "warning" | "error" | "neutral" | "info"
|
|
15
|
+
|
|
16
|
+
const pillVariants = cva(
|
|
17
|
+
"inline-flex w-fit shrink-0 items-center justify-center gap-1 whitespace-nowrap rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors [&>svg]:size-3",
|
|
18
|
+
{
|
|
19
|
+
variants: {
|
|
20
|
+
variant: {
|
|
21
|
+
default: "border-transparent bg-primary text-primary-foreground",
|
|
22
|
+
secondary: "border-transparent bg-secondary text-secondary-foreground",
|
|
23
|
+
destructive: "border-transparent bg-destructive text-white dark:bg-destructive/60",
|
|
24
|
+
outline: "border-border bg-background text-foreground",
|
|
25
|
+
ghost: "border-transparent bg-transparent text-muted-foreground",
|
|
26
|
+
success: "border-transparent bg-green-100 text-green-950 dark:bg-green-950 dark:text-green-100",
|
|
27
|
+
warning: "border-transparent bg-yellow-100 text-yellow-950 dark:bg-yellow-950 dark:text-yellow-100",
|
|
28
|
+
error: "border-transparent bg-red-100 text-red-950 dark:bg-red-950 dark:text-red-100",
|
|
29
|
+
neutral: "border-transparent bg-muted text-foreground",
|
|
30
|
+
info: "border-transparent bg-blue-100 text-blue-950 dark:bg-blue-950 dark:text-blue-100",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
defaultVariants: {
|
|
34
|
+
variant: "neutral",
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
export interface PillProps
|
|
40
|
+
extends React.ComponentProps<"span">,
|
|
41
|
+
VariantProps<typeof pillVariants> {}
|
|
42
|
+
|
|
43
|
+
export function Pill({ className, variant = "neutral", ...props }: PillProps) {
|
|
44
|
+
return (
|
|
45
|
+
<span
|
|
46
|
+
data-slot="pill"
|
|
47
|
+
data-variant={variant}
|
|
48
|
+
className={cn(pillVariants({ variant }), className)}
|
|
49
|
+
{...props}
|
|
50
|
+
/>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface StatusPillProps extends Omit<PillProps, "variant"> {
|
|
55
|
+
status: React.ReactNode
|
|
56
|
+
intent?: PillStatus
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function StatusPill({ status, intent = "neutral", children, ...props }: StatusPillProps) {
|
|
60
|
+
return (
|
|
61
|
+
<Pill data-slot="status-pill" variant={intent} {...props}>
|
|
62
|
+
{children ?? status}
|
|
63
|
+
</Pill>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { pillVariants }
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
|
|
5
|
+
import { cn } from "../lib/utils"
|
|
6
|
+
|
|
7
|
+
export interface QuickSegmentProps
|
|
8
|
+
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onSelect" | "value"> {
|
|
9
|
+
label: React.ReactNode
|
|
10
|
+
value: string
|
|
11
|
+
selected?: boolean
|
|
12
|
+
count?: number | string
|
|
13
|
+
description?: React.ReactNode
|
|
14
|
+
onSelect?: (value: string) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function QuickSegment({
|
|
18
|
+
label,
|
|
19
|
+
value,
|
|
20
|
+
selected = false,
|
|
21
|
+
count,
|
|
22
|
+
description,
|
|
23
|
+
onSelect,
|
|
24
|
+
className,
|
|
25
|
+
type = "button",
|
|
26
|
+
...props
|
|
27
|
+
}: QuickSegmentProps) {
|
|
28
|
+
return (
|
|
29
|
+
<button
|
|
30
|
+
data-slot="quick-segment"
|
|
31
|
+
data-selected={selected ? "true" : "false"}
|
|
32
|
+
type={type}
|
|
33
|
+
aria-pressed={selected}
|
|
34
|
+
className={cn(
|
|
35
|
+
"inline-flex min-h-8 items-center gap-2 rounded-full border px-3 py-1.5 text-sm font-medium transition-colors",
|
|
36
|
+
selected
|
|
37
|
+
? "border-primary bg-primary text-primary-foreground shadow-sm"
|
|
38
|
+
: "border-border bg-background text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
39
|
+
className
|
|
40
|
+
)}
|
|
41
|
+
onClick={(event) => {
|
|
42
|
+
props.onClick?.(event)
|
|
43
|
+
if (!event.defaultPrevented) onSelect?.(value)
|
|
44
|
+
}}
|
|
45
|
+
{...props}
|
|
46
|
+
>
|
|
47
|
+
<span data-slot="quick-segment-label">{label}</span>
|
|
48
|
+
{count !== undefined ? (
|
|
49
|
+
<span
|
|
50
|
+
data-slot="quick-segment-count"
|
|
51
|
+
className={cn(
|
|
52
|
+
"rounded-full px-1.5 py-0.5 text-[11px] leading-none",
|
|
53
|
+
selected
|
|
54
|
+
? "bg-primary-foreground/20 text-primary-foreground"
|
|
55
|
+
: "bg-muted text-muted-foreground"
|
|
56
|
+
)}
|
|
57
|
+
>
|
|
58
|
+
{count}
|
|
59
|
+
</span>
|
|
60
|
+
) : null}
|
|
61
|
+
{description ? (
|
|
62
|
+
<span data-slot="quick-segment-description" className="sr-only">
|
|
63
|
+
{description}
|
|
64
|
+
</span>
|
|
65
|
+
) : null}
|
|
66
|
+
</button>
|
|
67
|
+
)
|
|
68
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -33,6 +33,7 @@ export * from "./components/data-table-filter"
|
|
|
33
33
|
export * from "./components/data-table-quick-views"
|
|
34
34
|
export * from "./components/data-table-toolbar"
|
|
35
35
|
export * from "./components/detail-view"
|
|
36
|
+
export * from "./components/detail-drawer"
|
|
36
37
|
export * from "./components/dialog"
|
|
37
38
|
export * from "./components/dropdown-menu"
|
|
38
39
|
export * from "./components/empty-state"
|
|
@@ -47,6 +48,8 @@ export * from "./components/inbox-toolbar"
|
|
|
47
48
|
export * from "./components/inline-banner"
|
|
48
49
|
export * from "./components/input"
|
|
49
50
|
export * from "./components/insights-filter-bar"
|
|
51
|
+
export * from "./components/days-open-cell"
|
|
52
|
+
export * from "./components/linked-entity-cell"
|
|
50
53
|
export * from "./components/item-list"
|
|
51
54
|
export * from "./components/item-list-display"
|
|
52
55
|
export * from "./components/item-list-filter"
|
|
@@ -56,9 +59,11 @@ export * from "./components/label"
|
|
|
56
59
|
export * from "./components/message"
|
|
57
60
|
export * from "./components/metric-card"
|
|
58
61
|
export * from "./components/performance-metrics-table"
|
|
62
|
+
export * from "./components/pill"
|
|
59
63
|
export * from "./components/preview-list"
|
|
60
64
|
export * from "./components/progress"
|
|
61
65
|
export * from "./components/quick-action-chat-area"
|
|
66
|
+
export * from "./components/quick-segment"
|
|
62
67
|
export {
|
|
63
68
|
QuickActionModal,
|
|
64
69
|
type QuickActionPriority,
|
|
@@ -3,41 +3,83 @@ import { describe, expect, it } from "vitest"
|
|
|
3
3
|
import { displayName, getInitials, shortName } from "../user-display"
|
|
4
4
|
|
|
5
5
|
describe("displayName", () => {
|
|
6
|
-
it("
|
|
6
|
+
it("returns first_name + last_name when both are present", () => {
|
|
7
7
|
expect(
|
|
8
|
-
displayName({ first_name: "Sarah", last_name: "Mitchell", name: "S Mitchell", email: "sarah@example.com" })
|
|
8
|
+
displayName({ first_name: "Sarah", last_name: "Mitchell", name: "S Mitchell", email: "sarah@example.com" })
|
|
9
9
|
).toBe("Sarah Mitchell")
|
|
10
10
|
})
|
|
11
11
|
|
|
12
|
-
it("
|
|
13
|
-
expect(displayName({ first_name: "Sarah", last_name: null, name: null, email: "sarah@example.com" })).toBe(
|
|
14
|
-
|
|
12
|
+
it("returns first_name alone when last_name is missing", () => {
|
|
13
|
+
expect(displayName({ first_name: "Sarah", last_name: null, name: null, email: "sarah@example.com" })).toBe(
|
|
14
|
+
"Sarah"
|
|
15
|
+
)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it("falls back to name when first_name is missing", () => {
|
|
19
|
+
expect(displayName({ first_name: null, last_name: null, name: "Sarah Mitchell", email: "sarah@example.com" })).toBe(
|
|
20
|
+
"Sarah Mitchell"
|
|
21
|
+
)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it("falls back to email local-part when name fields are missing or empty", () => {
|
|
15
25
|
expect(displayName({ first_name: "", last_name: "", name: "", email: "sarah@example.com" })).toBe("sarah")
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it("falls back to Unknown user when profile fields are absent", () => {
|
|
16
29
|
expect(displayName({})).toBe("Unknown user")
|
|
17
30
|
expect(displayName()).toBe("Unknown user")
|
|
18
31
|
})
|
|
32
|
+
|
|
33
|
+
it("ignores last_name if first_name is missing", () => {
|
|
34
|
+
expect(displayName({ first_name: null, last_name: "Mitchell", name: "Old Name", email: "sarah@example.com" })).toBe(
|
|
35
|
+
"Old Name"
|
|
36
|
+
)
|
|
37
|
+
})
|
|
19
38
|
})
|
|
20
39
|
|
|
21
40
|
describe("getInitials", () => {
|
|
22
|
-
it("
|
|
41
|
+
it("returns uppercased initials from first_name and last_name", () => {
|
|
23
42
|
expect(getInitials({ first_name: "joe", last_name: "kim", email: "joe@example.com" })).toBe("JK")
|
|
24
43
|
})
|
|
25
44
|
|
|
26
|
-
it("
|
|
27
|
-
expect(getInitials({ first_name: null, last_name: null, name: "Sarah Mitchell", email: "sarah@example.com" })).toBe(
|
|
45
|
+
it("splits name into two parts for initials when first/last are not set", () => {
|
|
46
|
+
expect(getInitials({ first_name: null, last_name: null, name: "Sarah Mitchell", email: "sarah@example.com" })).toBe(
|
|
47
|
+
"SM"
|
|
48
|
+
)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it("uses first two characters of a single-word name", () => {
|
|
28
52
|
expect(getInitials({ first_name: null, last_name: null, name: "Sarah", email: "sarah@example.com" })).toBe("SA")
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it("falls back to first two email local characters when no name fields are set", () => {
|
|
29
56
|
expect(getInitials({ first_name: null, last_name: null, name: null, email: "zz@example.com" })).toBe("ZZ")
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it("returns question mark when no initials can be derived", () => {
|
|
30
60
|
expect(getInitials({})).toBe("?")
|
|
31
61
|
expect(getInitials()).toBe("?")
|
|
32
62
|
})
|
|
63
|
+
|
|
64
|
+
it("handles multi-word name taking the first two initials", () => {
|
|
65
|
+
expect(getInitials({ name: "Mary Jane Watson", email: "mj@example.com" })).toBe("MJ")
|
|
66
|
+
})
|
|
33
67
|
})
|
|
34
68
|
|
|
35
69
|
describe("shortName", () => {
|
|
36
|
-
it("returns
|
|
70
|
+
it("returns 'First L.' when first and last are present", () => {
|
|
37
71
|
expect(shortName({ first_name: "Sarah", last_name: "Mitchell", email: "sarah@example.com" })).toBe("Sarah M.")
|
|
38
72
|
})
|
|
39
73
|
|
|
40
|
-
it("falls back to displayName when
|
|
41
|
-
expect(shortName({
|
|
74
|
+
it("falls back to displayName when last_name is unavailable", () => {
|
|
75
|
+
expect(shortName({ first_name: "Sarah", last_name: null, name: null, email: "sarah@example.com" })).toBe("Sarah")
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it("falls back to email local-part when all names are missing", () => {
|
|
79
|
+
expect(shortName({ first_name: null, last_name: null, name: null, email: "sarah@example.com" })).toBe("sarah")
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it("falls back to Unknown user when no profile fields are present", () => {
|
|
83
|
+
expect(shortName({})).toBe("Unknown user")
|
|
42
84
|
})
|
|
43
85
|
})
|