@exxatdesignux/ui 0.2.18 → 0.2.19
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/CHANGELOG.md +15 -0
- package/consumer-extras/AGENTS.md +76 -0
- package/consumer-extras/README.md +5 -1
- package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +14 -3
- package/consumer-extras/cursor-skills/exxat-consumer-app/SKILL.md +37 -0
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +21 -6
- package/consumer-extras/cursor-skills/exxat-focused-workflow-page/SKILL.md +57 -0
- package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +4 -2
- package/consumer-extras/patterns/consumer-app-pattern.md +39 -0
- package/consumer-extras/patterns/consumer-upgrade-checklist.md +20 -0
- package/consumer-extras/patterns/data-views-pattern.md +40 -3
- package/consumer-extras/patterns/focused-workflow-page-pattern.md +84 -0
- package/consumer-extras/patterns/shell-surface-elevation-pattern.md +5 -3
- package/package.json +2 -1
- package/src/components/ui/button-group.tsx +81 -0
- package/src/components/ui/button.tsx +4 -4
- package/src/globals.css +7 -1858
- package/src/theme.css +10 -1126
- package/src/tokens/README.md +15 -0
- package/src/tokens/base.css +337 -0
- package/src/tokens/high-contrast.css +1195 -0
- package/src/tokens/layers.css +224 -0
- package/src/tokens/tailwind-bridge.css +118 -0
- package/src/tokens/themes.css +201 -0
- package/template/AGENTS.md +60 -22
- package/template/app/(app)/dashboard/loading.tsx +3 -15
- package/template/app/(app)/dashboard/page.tsx +2 -14
- package/template/app/(app)/data-list/layout.tsx +43 -0
- package/template/app/(app)/data-list/page.tsx +2 -2
- package/template/app/(app)/examples/focused-workflow/page.tsx +5 -0
- package/template/app/(app)/examples/page.tsx +1 -0
- package/template/app/(app)/loading.tsx +1 -18
- package/template/app/(app)/question-bank/find/page.tsx +2 -1
- package/template/app/(app)/question-bank/library/page.tsx +2 -1
- package/template/app/(app)/question-bank/list/page.tsx +2 -1
- package/template/app/(app)/question-bank/new/page.tsx +15 -23
- package/template/app/(app)/question-bank/page.tsx +2 -1
- package/template/app/(app)/settings/page.tsx +4 -5
- package/template/app/globals.css +7 -1964
- package/template/components/app-route-loading.tsx +14 -0
- package/template/components/app-sidebar.tsx +70 -55
- package/template/components/data-views/index.ts +37 -9
- package/template/components/data-views/list-page-calendar-view.tsx +593 -0
- package/template/components/data-views/list-page-connected-view-body.tsx +66 -0
- package/template/components/data-views/list-page-folder-columns-panel.tsx +345 -0
- package/template/components/data-views/list-page-split-hub-chrome.tsx +8 -0
- package/template/components/examples/focused-workflow-showcase.tsx +183 -0
- package/template/components/list-hub-board-view.tsx +68 -0
- package/template/components/list-hub-client.tsx +186 -0
- package/template/components/list-hub-list-view.tsx +36 -0
- package/template/components/list-hub-panel-activator.tsx +8 -0
- package/template/components/list-hub-secondary-nav.tsx +121 -0
- package/template/components/list-hub-table.tsx +336 -0
- package/template/components/new-question-composer.tsx +6 -24
- package/template/components/product-switcher.tsx +3 -2
- package/template/components/question-bank-client.tsx +4 -1
- package/template/components/question-bank-folder-columns-panel.tsx +104 -0
- package/template/components/question-bank-table.tsx +143 -485
- package/template/components/secondary-panel/nav-link-rows.tsx +83 -0
- package/template/components/secondary-panel.tsx +4 -44
- package/template/components/secondary-panels/list-hub-panel.tsx +39 -0
- package/template/components/secondary-panels/question-bank-panel.tsx +39 -0
- package/template/components/secondary-panels/registry.tsx +15 -0
- package/template/components/settings-appearance-card.tsx +3 -2
- package/template/components/settings-client.tsx +59 -15
- package/template/components/settings-form-row.tsx +9 -4
- package/template/components/table-properties/drawer-button.tsx +13 -0
- package/template/components/table-properties/drawer.tsx +65 -4
- package/template/components/templates/focused-workflow-layouts.tsx +448 -0
- package/template/components/templates/focused-workflow-page-template.tsx +69 -0
- package/template/components/templates/list-page.tsx +29 -5
- package/template/components/templates/nested-secondary-panel-shell.tsx +2 -1
- package/template/components/templates/page-loading-shell.tsx +262 -0
- package/template/components/ui/button-group.tsx +1 -0
- package/template/docs/consumer-app-pattern.md +39 -0
- package/template/docs/data-views-pattern.md +40 -3
- package/template/docs/drawer-vs-dialog-pattern.md +3 -1
- package/template/docs/focused-workflow-page-pattern.md +84 -0
- package/template/docs/shell-surface-elevation-pattern.md +5 -3
- package/template/lib/command-menu-search-data.ts +11 -27
- package/template/lib/data-list-display-options.ts +16 -2
- package/template/lib/data-list-view-registry.ts +104 -0
- package/template/lib/data-list-view-surface.ts +15 -1
- package/template/lib/data-list-view.ts +10 -1
- package/template/lib/data-view-dashboard-storage.ts +38 -35
- package/template/lib/hub-connected-view-renderers.ts +58 -0
- package/template/lib/list-hub-nav.ts +121 -0
- package/template/lib/list-hub-supported-views.ts +10 -0
- package/template/lib/list-page-table-properties.ts +3 -7
- package/template/lib/list-status-badges.ts +4 -97
- package/template/lib/mock/list-hub-directory.ts +27 -0
- package/template/lib/mock/list-hub-kpi.ts +27 -0
- package/template/lib/mock/navigation.tsx +1 -0
- package/template/lib/page-loading-variant.ts +40 -0
- package/template/lib/question-bank-supported-views.ts +13 -0
- package/template/lib/table-state-lifecycle.ts +2 -2
- package/template/app/(app)/data-list/[id]/page.tsx +0 -44
- package/template/app/(app)/data-list/new/page.tsx +0 -34
- package/template/components/compliance-board-view.tsx +0 -142
- package/template/components/compliance-client.tsx +0 -92
- package/template/components/compliance-list-view.tsx +0 -54
- package/template/components/compliance-page-header.tsx +0 -89
- package/template/components/compliance-table.tsx +0 -612
- package/template/components/data-view-dashboard-charts-compliance.tsx +0 -963
- package/template/components/data-view-dashboard-charts-team.tsx +0 -971
- package/template/components/data-view-dashboard-charts.tsx +0 -1503
- package/template/components/new-placement-back-btn.tsx +0 -28
- package/template/components/new-placement-form.tsx +0 -1068
- package/template/components/placement-board-card.tsx +0 -262
- package/template/components/placement-detail.tsx +0 -438
- package/template/components/placements-board-view.tsx +0 -404
- package/template/components/placements-client.tsx +0 -252
- package/template/components/placements-list-view.tsx +0 -171
- package/template/components/placements-page-header.tsx +0 -166
- package/template/components/placements-table-cells.test.tsx +0 -22
- package/template/components/placements-table-cells.tsx +0 -173
- package/template/components/placements-table-columns.tsx +0 -640
- package/template/components/placements-table.tsx +0 -1642
- package/template/components/rotations-empty-state.tsx +0 -50
- package/template/components/rotations-panel-activator.tsx +0 -8
- package/template/components/sites-all-client.tsx +0 -154
- package/template/components/sites-board-view.tsx +0 -67
- package/template/components/sites-list-view.tsx +0 -42
- package/template/components/sites-table.tsx +0 -382
- package/template/components/team-board-view.tsx +0 -122
- package/template/components/team-client.tsx +0 -100
- package/template/components/team-list-view.tsx +0 -59
- package/template/components/team-page-header.tsx +0 -92
- package/template/components/team-table.tsx +0 -693
- package/template/lib/data-view-dashboard-placements-layout.ts +0 -215
- package/template/lib/mock/compliance-kpi.ts +0 -61
- package/template/lib/mock/compliance.ts +0 -146
- package/template/lib/mock/placements-kpi.ts +0 -134
- package/template/lib/mock/placements.ts +0 -183
- package/template/lib/mock/sites-directory.ts +0 -16
- package/template/lib/mock/sites-kpi.ts +0 -25
- package/template/lib/mock/team-kpi.ts +0 -60
- package/template/lib/mock/team.ts +0 -118
- package/template/lib/placement-board-card-layout.ts +0 -79
- package/template/lib/placement-lifecycle.ts +0 -5
|
@@ -1,693 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Team roster — DataTable + TablePropertiesDrawer + table/list/board/panel/dashboard (shared `tableState.rows`).
|
|
5
|
-
* Dashboard view uses `TeamDashboardChartsSection` (customise on canvas) + lib/mock/team-kpi.
|
|
6
|
-
* Panel view uses `FinderPanelView` with status-based groups (Active, Away, Invited).
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import * as React from "react"
|
|
10
|
-
import { AvatarInitials } from "@/components/ui/avatar"
|
|
11
|
-
import {
|
|
12
|
-
TEAM_MEMBER_STATUS_BADGE_CLASS,
|
|
13
|
-
TEAM_MEMBER_STATUS_ICON,
|
|
14
|
-
TEAM_MEMBER_STATUS_LABEL,
|
|
15
|
-
} from "@/lib/list-status-badges"
|
|
16
|
-
import { mailtoHref } from "@/lib/mailto"
|
|
17
|
-
import type { TeamMember } from "@/lib/mock/team"
|
|
18
|
-
import { DataTable, DataTableToolbar } from "@/components/data-table"
|
|
19
|
-
import {
|
|
20
|
-
TeamDashboardChartsSection,
|
|
21
|
-
DEFAULT_TEAM_CHART_TYPES,
|
|
22
|
-
DEFAULT_TEAM_SPANS,
|
|
23
|
-
ALL_TEAM_DASHBOARD_CARDS,
|
|
24
|
-
loadTeamDashboardLayout,
|
|
25
|
-
mergeTeamDashboardLayout,
|
|
26
|
-
saveTeamDashboardLayout,
|
|
27
|
-
} from "@/components/data-view-dashboard-charts-team"
|
|
28
|
-
import { KEY_METRICS_KPI_COUNT_DEFAULT } from "@/lib/dashboard-layout-merge"
|
|
29
|
-
import type { ChartType, DashboardLayout } from "@/lib/data-view-dashboard-placements-layout"
|
|
30
|
-
import { TeamListView } from "@/components/team-list-view"
|
|
31
|
-
import { TeamBoardView, TEAM_BOARD_GROUP_OPTIONS } from "@/components/team-board-view"
|
|
32
|
-
import { FinderPanelView, type FinderGroup } from "@/components/data-views/finder-panel-view"
|
|
33
|
-
import { ListPageSplitHubChrome } from "@/components/data-views/list-page-split-hub-chrome"
|
|
34
|
-
import { teamKpiInsight, teamKpiMetrics } from "@/lib/mock/team-kpi"
|
|
35
|
-
import { useRouter } from "next/navigation"
|
|
36
|
-
import { cn } from "@/lib/utils"
|
|
37
|
-
import type { DataListViewType } from "@/lib/data-list-view"
|
|
38
|
-
import type { OpenTablePropertiesHandle } from "@/lib/list-page-table-properties"
|
|
39
|
-
import type { ColumnDef } from "@/components/data-table/types"
|
|
40
|
-
import { useTableState } from "@/components/data-table/use-table-state"
|
|
41
|
-
import { TablePropertiesDrawerButton } from "@/components/table-properties"
|
|
42
|
-
import type { ConditionalRule, FilterFieldDef, FilterOperator } from "@/components/table-properties/types"
|
|
43
|
-
import { ListHubStatusBadge } from "@/components/list-hub-status-badge"
|
|
44
|
-
import { Button } from "@/components/ui/button"
|
|
45
|
-
import {
|
|
46
|
-
DropdownMenu,
|
|
47
|
-
DropdownMenuContent,
|
|
48
|
-
DropdownMenuItem,
|
|
49
|
-
DropdownMenuTrigger,
|
|
50
|
-
} from "@/components/ui/dropdown-menu"
|
|
51
|
-
import { Tip } from "@/components/ui/tip"
|
|
52
|
-
import { CoachMark } from "@/components/ui/coach-mark"
|
|
53
|
-
import { useCoachMark } from "@/hooks/use-coach-mark"
|
|
54
|
-
import { DASHBOARD_CUSTOMIZE_COACH_STEPS } from "@/lib/dashboard-customize-coach-mark"
|
|
55
|
-
import {
|
|
56
|
-
DEFAULT_DATA_LIST_DISPLAY_OPTIONS,
|
|
57
|
-
type DataListDisplayOptions,
|
|
58
|
-
} from "@/lib/data-list-display-options"
|
|
59
|
-
|
|
60
|
-
function uniqueRoles(members: TeamMember[]) {
|
|
61
|
-
return [...new Set(members.map(m => m.role))].sort().map(r => ({ value: r, label: r }))
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function formatUsPhoneDigits(digits: string) {
|
|
65
|
-
const d = digits.replace(/\D/g, "").slice(0, 10)
|
|
66
|
-
if (d.length !== 10) return digits
|
|
67
|
-
return `(${d.slice(0, 3)}) ${d.slice(3, 6)}-${d.slice(6)}`
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const STATUS_FILTER_OPTS = [
|
|
71
|
-
{ value: "active", label: TEAM_MEMBER_STATUS_LABEL.active },
|
|
72
|
-
{ value: "away", label: TEAM_MEMBER_STATUS_LABEL.away },
|
|
73
|
-
{ value: "invited", label: TEAM_MEMBER_STATUS_LABEL.invited },
|
|
74
|
-
]
|
|
75
|
-
|
|
76
|
-
// ─── Team-specific panel view helpers ──────────────────────────────────────
|
|
77
|
-
|
|
78
|
-
function TeamFinderListRow({
|
|
79
|
-
member,
|
|
80
|
-
isSelected,
|
|
81
|
-
}: {
|
|
82
|
-
member: TeamMember
|
|
83
|
-
isSelected: boolean
|
|
84
|
-
}) {
|
|
85
|
-
return (
|
|
86
|
-
<div
|
|
87
|
-
className={`flex w-full min-w-0 items-center gap-3 transition-colors duration-75 ${
|
|
88
|
-
isSelected ? "bg-transparent text-accent-foreground" : "text-foreground"
|
|
89
|
-
}`}
|
|
90
|
-
>
|
|
91
|
-
<AvatarInitials
|
|
92
|
-
initials={member.initials}
|
|
93
|
-
className={cn(
|
|
94
|
-
"size-8 shrink-0 rounded-full text-[11px] font-semibold",
|
|
95
|
-
isSelected ? "ring-2 ring-accent-foreground/35" : "",
|
|
96
|
-
)}
|
|
97
|
-
/>
|
|
98
|
-
<div className="min-w-0 flex-1">
|
|
99
|
-
<p className={cn("truncate text-[13px] font-medium leading-tight", isSelected ? "text-accent-foreground" : "text-foreground")}>
|
|
100
|
-
{member.name}
|
|
101
|
-
</p>
|
|
102
|
-
<p className={cn("mt-0.5 truncate text-[11px] leading-tight", isSelected ? "text-accent-foreground/80" : "text-muted-foreground")}>
|
|
103
|
-
{member.role}
|
|
104
|
-
</p>
|
|
105
|
-
</div>
|
|
106
|
-
{!isSelected && (
|
|
107
|
-
<ListHubStatusBadge
|
|
108
|
-
label={TEAM_MEMBER_STATUS_LABEL[member.status]}
|
|
109
|
-
tintClassName={TEAM_MEMBER_STATUS_BADGE_CLASS[member.status]}
|
|
110
|
-
icon={TEAM_MEMBER_STATUS_ICON[member.status]}
|
|
111
|
-
/>
|
|
112
|
-
)}
|
|
113
|
-
</div>
|
|
114
|
-
)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function TeamFinderDetail({
|
|
118
|
-
member,
|
|
119
|
-
}: {
|
|
120
|
-
member: TeamMember
|
|
121
|
-
}) {
|
|
122
|
-
const router = useRouter()
|
|
123
|
-
|
|
124
|
-
return (
|
|
125
|
-
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
126
|
-
{/* Header */}
|
|
127
|
-
<div className="flex shrink-0 items-start gap-4 border-b border-border px-5 py-4">
|
|
128
|
-
<AvatarInitials initials={member.initials} className="size-14 shrink-0 rounded-full text-lg font-semibold" />
|
|
129
|
-
<div className="min-w-0 flex-1">
|
|
130
|
-
<h2 className="text-base font-semibold text-foreground leading-tight">{member.name}</h2>
|
|
131
|
-
<p className="mt-0.5 text-[13px] text-muted-foreground">{member.role}</p>
|
|
132
|
-
<div className="mt-2">
|
|
133
|
-
<ListHubStatusBadge
|
|
134
|
-
label={TEAM_MEMBER_STATUS_LABEL[member.status]}
|
|
135
|
-
tintClassName={TEAM_MEMBER_STATUS_BADGE_CLASS[member.status]}
|
|
136
|
-
icon={TEAM_MEMBER_STATUS_ICON[member.status]}
|
|
137
|
-
/>
|
|
138
|
-
</div>
|
|
139
|
-
</div>
|
|
140
|
-
<Tip side="bottom" label="Open full profile">
|
|
141
|
-
<Button type="button" variant="outline" size="sm" className="shrink-0"
|
|
142
|
-
onClick={() => router.push(`/team/${member.id}`)}
|
|
143
|
-
aria-label={`Open full profile for ${member.name}`}>
|
|
144
|
-
<i className="fa-light fa-arrow-up-right-from-square text-[12px]" aria-hidden="true" />
|
|
145
|
-
Open
|
|
146
|
-
</Button>
|
|
147
|
-
</Tip>
|
|
148
|
-
</div>
|
|
149
|
-
|
|
150
|
-
{/* Fields */}
|
|
151
|
-
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
|
|
152
|
-
<dl className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
153
|
-
<div className="flex flex-col gap-0.5">
|
|
154
|
-
<dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
|
|
155
|
-
<i className="fa-light fa-envelope text-[10px]" aria-hidden="true" /> Email
|
|
156
|
-
</dt>
|
|
157
|
-
<dd className="text-[13px]">
|
|
158
|
-
<a href={mailtoHref(member.email)} className="text-interactive-foreground hover:underline">{member.email}</a>
|
|
159
|
-
</dd>
|
|
160
|
-
</div>
|
|
161
|
-
<div className="flex flex-col gap-0.5">
|
|
162
|
-
<dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
|
|
163
|
-
<i className="fa-light fa-briefcase text-[10px]" aria-hidden="true" /> Role
|
|
164
|
-
</dt>
|
|
165
|
-
<dd className="text-[13px] text-foreground">{member.role}</dd>
|
|
166
|
-
</div>
|
|
167
|
-
<div className="flex flex-col gap-0.5">
|
|
168
|
-
<dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
|
|
169
|
-
<i className="fa-light fa-phone text-[10px]" aria-hidden="true" /> Phone
|
|
170
|
-
</dt>
|
|
171
|
-
<dd className="text-[13px] text-foreground">{member.phone}</dd>
|
|
172
|
-
</div>
|
|
173
|
-
</dl>
|
|
174
|
-
</div>
|
|
175
|
-
</div>
|
|
176
|
-
)
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// ─── Status groups for team panel view ────────────────────────────────────
|
|
180
|
-
|
|
181
|
-
const TEAM_STATUS_GROUPS: Array<{ id: string; label: string; accent: string }> = [
|
|
182
|
-
{ id: "all", label: "All", accent: "bg-muted-foreground" },
|
|
183
|
-
{ id: "active", label: "Active", accent: "bg-success" },
|
|
184
|
-
{ id: "away", label: "Away", accent: "bg-warning" },
|
|
185
|
-
{ id: "invited", label: "Invited", accent: "bg-brand" },
|
|
186
|
-
]
|
|
187
|
-
|
|
188
|
-
function buildTeamStatusGroups(members: TeamMember[]): FinderGroup[] {
|
|
189
|
-
return TEAM_STATUS_GROUPS.map(sg => ({
|
|
190
|
-
id: sg.id,
|
|
191
|
-
label: sg.label,
|
|
192
|
-
accent: sg.accent,
|
|
193
|
-
count: sg.id === "all" ? members.length : members.filter(m => m.status === sg.id).length,
|
|
194
|
-
}))
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function columnToFilterFieldDef(c: ColumnDef<TeamMember>): FilterFieldDef | null {
|
|
198
|
-
if (!c.filter) return null
|
|
199
|
-
const f = c.filter
|
|
200
|
-
const defaultOps: FilterOperator[] =
|
|
201
|
-
f.type === "select" || f.type === "date"
|
|
202
|
-
? ["is", "is_not"]
|
|
203
|
-
: ["contains", "not_contains"]
|
|
204
|
-
return {
|
|
205
|
-
key: c.key,
|
|
206
|
-
label: c.label,
|
|
207
|
-
icon: f.icon ?? "fa-filter",
|
|
208
|
-
type: f.type,
|
|
209
|
-
operators: (f.operators ?? defaultOps) as FilterOperator[],
|
|
210
|
-
options: f.options,
|
|
211
|
-
...(f.textMask ? { textMask: f.textMask } : {}),
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function columnsToFilterFields(cols: ColumnDef<TeamMember>[]) {
|
|
216
|
-
return cols.map(columnToFilterFieldDef).filter((x): x is FilterFieldDef => x !== null)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function buildTeamColumns(members: TeamMember[]): ColumnDef<TeamMember>[] {
|
|
220
|
-
const roleOpts = uniqueRoles(members)
|
|
221
|
-
|
|
222
|
-
const COLUMN_SELECT: ColumnDef<TeamMember> = {
|
|
223
|
-
key: "select",
|
|
224
|
-
label: "",
|
|
225
|
-
width: 40,
|
|
226
|
-
minWidth: 40,
|
|
227
|
-
defaultPin: "left",
|
|
228
|
-
lockPin: true,
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const cols: ColumnDef<TeamMember>[] = [
|
|
232
|
-
COLUMN_SELECT,
|
|
233
|
-
{
|
|
234
|
-
key: "name",
|
|
235
|
-
label: "Name",
|
|
236
|
-
width: 240,
|
|
237
|
-
minWidth: 160,
|
|
238
|
-
sortable: true,
|
|
239
|
-
sortKey: "name",
|
|
240
|
-
defaultPin: "left",
|
|
241
|
-
filter: {
|
|
242
|
-
type: "text",
|
|
243
|
-
icon: "fa-user",
|
|
244
|
-
operators: ["contains", "not_contains"],
|
|
245
|
-
},
|
|
246
|
-
cell: row => (
|
|
247
|
-
<div className="flex items-center gap-2.5 min-w-0">
|
|
248
|
-
<AvatarInitials initials={row.initials} className="size-8 shrink-0 text-xs" />
|
|
249
|
-
<span className="truncate text-sm font-medium text-foreground">{row.name}</span>
|
|
250
|
-
</div>
|
|
251
|
-
),
|
|
252
|
-
},
|
|
253
|
-
{
|
|
254
|
-
key: "role",
|
|
255
|
-
label: "Role",
|
|
256
|
-
width: 200,
|
|
257
|
-
minWidth: 140,
|
|
258
|
-
sortable: true,
|
|
259
|
-
sortKey: "role",
|
|
260
|
-
filter: {
|
|
261
|
-
type: "select",
|
|
262
|
-
icon: "fa-briefcase",
|
|
263
|
-
operators: ["is", "is_not"],
|
|
264
|
-
options: roleOpts,
|
|
265
|
-
},
|
|
266
|
-
cell: row => <span className="text-sm text-foreground/90">{row.role}</span>,
|
|
267
|
-
},
|
|
268
|
-
{
|
|
269
|
-
key: "email",
|
|
270
|
-
label: "Email",
|
|
271
|
-
width: 260,
|
|
272
|
-
minWidth: 180,
|
|
273
|
-
sortable: true,
|
|
274
|
-
sortKey: "email",
|
|
275
|
-
filter: {
|
|
276
|
-
type: "text",
|
|
277
|
-
icon: "fa-envelope",
|
|
278
|
-
operators: ["contains", "not_contains"],
|
|
279
|
-
},
|
|
280
|
-
cell: row => (
|
|
281
|
-
<a href={mailtoHref(row.email)} className="text-sm text-primary hover:underline truncate block">
|
|
282
|
-
{row.email}
|
|
283
|
-
</a>
|
|
284
|
-
),
|
|
285
|
-
},
|
|
286
|
-
{
|
|
287
|
-
key: "phone",
|
|
288
|
-
label: "Phone",
|
|
289
|
-
width: 148,
|
|
290
|
-
minWidth: 132,
|
|
291
|
-
sortable: true,
|
|
292
|
-
sortKey: "phone",
|
|
293
|
-
filter: {
|
|
294
|
-
type: "text",
|
|
295
|
-
icon: "fa-phone",
|
|
296
|
-
operators: ["contains", "not_contains"],
|
|
297
|
-
textMask: "phone",
|
|
298
|
-
},
|
|
299
|
-
cell: row => (
|
|
300
|
-
<a
|
|
301
|
-
href={`tel:+1${row.phone}`}
|
|
302
|
-
className="text-sm tabular-nums text-foreground/90 hover:text-primary hover:underline truncate block"
|
|
303
|
-
>
|
|
304
|
-
{formatUsPhoneDigits(row.phone)}
|
|
305
|
-
</a>
|
|
306
|
-
),
|
|
307
|
-
},
|
|
308
|
-
{
|
|
309
|
-
key: "status",
|
|
310
|
-
label: "Status",
|
|
311
|
-
width: 120,
|
|
312
|
-
minWidth: 100,
|
|
313
|
-
sortable: true,
|
|
314
|
-
sortKey: "status",
|
|
315
|
-
filter: {
|
|
316
|
-
type: "select",
|
|
317
|
-
icon: "fa-circle-dot",
|
|
318
|
-
operators: ["is", "is_not"],
|
|
319
|
-
options: STATUS_FILTER_OPTS,
|
|
320
|
-
},
|
|
321
|
-
cell: row => (
|
|
322
|
-
<ListHubStatusBadge
|
|
323
|
-
label={TEAM_MEMBER_STATUS_LABEL[row.status]}
|
|
324
|
-
tintClassName={TEAM_MEMBER_STATUS_BADGE_CLASS[row.status]}
|
|
325
|
-
icon={TEAM_MEMBER_STATUS_ICON[row.status]}
|
|
326
|
-
/>
|
|
327
|
-
),
|
|
328
|
-
},
|
|
329
|
-
{
|
|
330
|
-
key: "actions",
|
|
331
|
-
label: "",
|
|
332
|
-
width: 48,
|
|
333
|
-
minWidth: 48,
|
|
334
|
-
defaultPin: "right",
|
|
335
|
-
lockPin: true,
|
|
336
|
-
cell: row => (
|
|
337
|
-
<div className="flex items-center justify-center">
|
|
338
|
-
<DropdownMenu>
|
|
339
|
-
<DropdownMenuTrigger asChild>
|
|
340
|
-
<Button size="icon-sm" variant="ghost" aria-label={`Actions for ${row.name}`}>
|
|
341
|
-
<i className="fa-light fa-ellipsis text-sm" aria-hidden="true" />
|
|
342
|
-
</Button>
|
|
343
|
-
</DropdownMenuTrigger>
|
|
344
|
-
<DropdownMenuContent align="end">
|
|
345
|
-
<DropdownMenuItem onClick={() => window.open(mailtoHref(row.email))}>
|
|
346
|
-
<i className="fa-light fa-envelope" aria-hidden="true" />
|
|
347
|
-
Email
|
|
348
|
-
</DropdownMenuItem>
|
|
349
|
-
<DropdownMenuItem disabled>
|
|
350
|
-
<i className="fa-light fa-user-gear" aria-hidden="true" />
|
|
351
|
-
Manage access
|
|
352
|
-
</DropdownMenuItem>
|
|
353
|
-
</DropdownMenuContent>
|
|
354
|
-
</DropdownMenu>
|
|
355
|
-
</div>
|
|
356
|
-
),
|
|
357
|
-
},
|
|
358
|
-
]
|
|
359
|
-
|
|
360
|
-
return cols
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
export type TeamTableHandle = OpenTablePropertiesHandle
|
|
365
|
-
|
|
366
|
-
export const TeamTable = React.forwardRef<
|
|
367
|
-
TeamTableHandle,
|
|
368
|
-
{ members: TeamMember[]; view?: DataListViewType; onViewChange?: (v: DataListViewType) => void }
|
|
369
|
-
>(function TeamTable({ members, view = "table", onViewChange }, ref) {
|
|
370
|
-
const columns = React.useMemo(() => buildTeamColumns(members), [members])
|
|
371
|
-
const filterFields = React.useMemo(() => columnsToFilterFields(columns), [columns])
|
|
372
|
-
const fieldDefinitionsForDrawer = React.useMemo(
|
|
373
|
-
() =>
|
|
374
|
-
columns
|
|
375
|
-
.filter(c => c.key !== "select" && c.key !== "actions")
|
|
376
|
-
.map(c => ({ key: c.key, label: c.label, sortable: !!(c.sortable && (c.sortKey ?? c.key)) })),
|
|
377
|
-
[columns],
|
|
378
|
-
)
|
|
379
|
-
|
|
380
|
-
const resolveColumnLabel = React.useCallback(
|
|
381
|
-
(key: string) => columns.find(c => c.key === key)?.label ?? key,
|
|
382
|
-
[columns],
|
|
383
|
-
)
|
|
384
|
-
|
|
385
|
-
const [displayOptions, setDisplayOptions] = React.useState<DataListDisplayOptions>(DEFAULT_DATA_LIST_DISPLAY_OPTIONS)
|
|
386
|
-
const patchDisplay = React.useCallback((patch: Partial<DataListDisplayOptions>) => {
|
|
387
|
-
setDisplayOptions(prev => ({ ...prev, ...patch }))
|
|
388
|
-
}, [])
|
|
389
|
-
|
|
390
|
-
const [conditionalRules, setConditionalRules] = React.useState<ConditionalRule[]>([])
|
|
391
|
-
|
|
392
|
-
const addConditionalRule = React.useCallback((rule: Omit<ConditionalRule, "id">) => {
|
|
393
|
-
setConditionalRules(prev => [...prev, { ...rule, id: `cr-${Date.now()}` }])
|
|
394
|
-
}, [])
|
|
395
|
-
const removeConditionalRule = React.useCallback((id: string) => {
|
|
396
|
-
setConditionalRules(prev => prev.filter(r => r.id !== id))
|
|
397
|
-
}, [])
|
|
398
|
-
const updateConditionalRule = React.useCallback((id: string, patch: Partial<ConditionalRule>) => {
|
|
399
|
-
setConditionalRules(prev => prev.map(r => r.id === id ? { ...r, ...patch } : r))
|
|
400
|
-
}, [])
|
|
401
|
-
|
|
402
|
-
const tableState = useTableState(members, columns, { key: "name", dir: "asc" })
|
|
403
|
-
|
|
404
|
-
const dashboardKpi = React.useMemo(
|
|
405
|
-
() => ({
|
|
406
|
-
metrics: teamKpiMetrics(tableState.rows),
|
|
407
|
-
insight: teamKpiInsight(tableState.rows),
|
|
408
|
-
}),
|
|
409
|
-
[tableState.rows],
|
|
410
|
-
)
|
|
411
|
-
|
|
412
|
-
const [visibleTeamCards, setVisibleTeamCards] = React.useState<string[]>(() => ALL_TEAM_DASHBOARD_CARDS.map(c => c.id))
|
|
413
|
-
const [teamCardOrder, setTeamCardOrder] = React.useState<string[]>(() => ALL_TEAM_DASHBOARD_CARDS.map(c => c.id))
|
|
414
|
-
const [teamCardSpans, setTeamCardSpans] = React.useState<Record<string, 1 | 2>>(() => ({ ...DEFAULT_TEAM_SPANS }))
|
|
415
|
-
const [teamCardChartTypes, setTeamCardChartTypes] = React.useState<Record<string, ChartType>>(() => ({ ...DEFAULT_TEAM_CHART_TYPES }))
|
|
416
|
-
const [teamKeyMetricsKpiCount, setTeamKeyMetricsKpiCount] = React.useState<number>(KEY_METRICS_KPI_COUNT_DEFAULT)
|
|
417
|
-
const [teamDashboardLayoutEdit, setTeamDashboardLayoutEdit] = React.useState(false)
|
|
418
|
-
const teamDashboardLayoutHydrated = React.useRef(false)
|
|
419
|
-
const teamDashboardLayoutEditBaselineRef = React.useRef<DashboardLayout | null>(null)
|
|
420
|
-
|
|
421
|
-
React.useEffect(() => {
|
|
422
|
-
const saved = loadTeamDashboardLayout()
|
|
423
|
-
const m = mergeTeamDashboardLayout(saved)
|
|
424
|
-
setVisibleTeamCards(m.visible)
|
|
425
|
-
setTeamCardOrder(m.order)
|
|
426
|
-
setTeamCardSpans(m.spans ?? { ...DEFAULT_TEAM_SPANS })
|
|
427
|
-
setTeamCardChartTypes(m.chartTypes ?? { ...DEFAULT_TEAM_CHART_TYPES })
|
|
428
|
-
setTeamKeyMetricsKpiCount(m.keyMetricsKpiCount ?? KEY_METRICS_KPI_COUNT_DEFAULT)
|
|
429
|
-
teamDashboardLayoutHydrated.current = true
|
|
430
|
-
}, [])
|
|
431
|
-
|
|
432
|
-
React.useEffect(() => {
|
|
433
|
-
if (!teamDashboardLayoutHydrated.current) return
|
|
434
|
-
saveTeamDashboardLayout({
|
|
435
|
-
visible: visibleTeamCards,
|
|
436
|
-
order: teamCardOrder,
|
|
437
|
-
spans: teamCardSpans,
|
|
438
|
-
chartTypes: teamCardChartTypes,
|
|
439
|
-
keyMetricsKpiCount: teamKeyMetricsKpiCount,
|
|
440
|
-
})
|
|
441
|
-
}, [visibleTeamCards, teamCardOrder, teamCardSpans, teamCardChartTypes, teamKeyMetricsKpiCount])
|
|
442
|
-
|
|
443
|
-
const handleTeamVisibleChange = React.useCallback((v: string[]) => {
|
|
444
|
-
setVisibleTeamCards(v)
|
|
445
|
-
}, [])
|
|
446
|
-
|
|
447
|
-
const handleTeamOrderChange = React.useCallback((o: string[]) => {
|
|
448
|
-
setTeamCardOrder(o)
|
|
449
|
-
}, [])
|
|
450
|
-
|
|
451
|
-
const handleTeamSpanChange = React.useCallback((id: string, span: 1 | 2) => {
|
|
452
|
-
setTeamCardSpans(prev => ({ ...prev, [id]: span }))
|
|
453
|
-
}, [])
|
|
454
|
-
|
|
455
|
-
const handleTeamChartTypeChange = React.useCallback((id: string, t: ChartType) => {
|
|
456
|
-
setTeamCardChartTypes(prev => ({ ...prev, [id]: t }))
|
|
457
|
-
}, [])
|
|
458
|
-
|
|
459
|
-
const handleResetTeamDashboardLayout = React.useCallback(() => {
|
|
460
|
-
setVisibleTeamCards(ALL_TEAM_DASHBOARD_CARDS.map(c => c.id))
|
|
461
|
-
setTeamCardOrder(ALL_TEAM_DASHBOARD_CARDS.map(c => c.id))
|
|
462
|
-
setTeamCardSpans({ ...DEFAULT_TEAM_SPANS })
|
|
463
|
-
setTeamCardChartTypes({ ...DEFAULT_TEAM_CHART_TYPES })
|
|
464
|
-
setTeamKeyMetricsKpiCount(KEY_METRICS_KPI_COUNT_DEFAULT)
|
|
465
|
-
}, [])
|
|
466
|
-
|
|
467
|
-
const handleTeamDashboardLayoutEditStart = React.useCallback(() => {
|
|
468
|
-
teamDashboardLayoutEditBaselineRef.current = {
|
|
469
|
-
visible: [...visibleTeamCards],
|
|
470
|
-
order: [...teamCardOrder],
|
|
471
|
-
spans: { ...teamCardSpans },
|
|
472
|
-
chartTypes: { ...teamCardChartTypes },
|
|
473
|
-
keyMetricsKpiCount: teamKeyMetricsKpiCount,
|
|
474
|
-
}
|
|
475
|
-
setTeamDashboardLayoutEdit(true)
|
|
476
|
-
}, [visibleTeamCards, teamCardOrder, teamCardSpans, teamCardChartTypes, teamKeyMetricsKpiCount])
|
|
477
|
-
|
|
478
|
-
const handleTeamDashboardLayoutEditDone = React.useCallback(() => {
|
|
479
|
-
setTeamDashboardLayoutEdit(false)
|
|
480
|
-
}, [])
|
|
481
|
-
|
|
482
|
-
const handleTeamDashboardLayoutEditCancel = React.useCallback(() => {
|
|
483
|
-
const b = teamDashboardLayoutEditBaselineRef.current
|
|
484
|
-
if (b) {
|
|
485
|
-
setVisibleTeamCards(b.visible)
|
|
486
|
-
setTeamCardOrder(b.order)
|
|
487
|
-
setTeamCardSpans(b.spans ?? { ...DEFAULT_TEAM_SPANS })
|
|
488
|
-
setTeamCardChartTypes(b.chartTypes ?? { ...DEFAULT_TEAM_CHART_TYPES })
|
|
489
|
-
setTeamKeyMetricsKpiCount(b.keyMetricsKpiCount ?? KEY_METRICS_KPI_COUNT_DEFAULT)
|
|
490
|
-
}
|
|
491
|
-
setTeamDashboardLayoutEdit(false)
|
|
492
|
-
}, [])
|
|
493
|
-
|
|
494
|
-
const dashboardCustomizeCoach = useCoachMark({
|
|
495
|
-
flowId: "team-dashboard-customize",
|
|
496
|
-
steps: DASHBOARD_CUSTOMIZE_COACH_STEPS,
|
|
497
|
-
delay: 700,
|
|
498
|
-
enabled: view === "dashboard",
|
|
499
|
-
})
|
|
500
|
-
|
|
501
|
-
React.useImperativeHandle(ref, () => ({
|
|
502
|
-
openPropertiesDrawer: () => {
|
|
503
|
-
tableState.setSheetOpen(true)
|
|
504
|
-
},
|
|
505
|
-
// `tableState` is freshly returned each render by useTableState; depending on
|
|
506
|
-
// it would re-create the imperative handle on every render. Only the React
|
|
507
|
-
// setter is needed (and is referentially stable).
|
|
508
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
509
|
-
}), [tableState.setSheetOpen])
|
|
510
|
-
|
|
511
|
-
const teamPanelFinderGroups = React.useMemo(
|
|
512
|
-
() => buildTeamStatusGroups(tableState.rows),
|
|
513
|
-
[tableState.rows],
|
|
514
|
-
)
|
|
515
|
-
|
|
516
|
-
const teamBoardGroupKey = TEAM_BOARD_GROUP_OPTIONS.some(
|
|
517
|
-
o => o.key === displayOptions.boardGroupByColumnKey,
|
|
518
|
-
)
|
|
519
|
-
? displayOptions.boardGroupByColumnKey
|
|
520
|
-
: "status"
|
|
521
|
-
|
|
522
|
-
const drawerToolbarProps = {
|
|
523
|
-
totalRows: members.length,
|
|
524
|
-
filterFields,
|
|
525
|
-
fieldDefinitions: fieldDefinitionsForDrawer,
|
|
526
|
-
resolveColumnLabel,
|
|
527
|
-
displayOptions,
|
|
528
|
-
onDisplayOptionsChange: patchDisplay,
|
|
529
|
-
conditionalRules,
|
|
530
|
-
onAddConditionalRule: addConditionalRule,
|
|
531
|
-
onRemoveConditionalRule: removeConditionalRule,
|
|
532
|
-
onUpdateConditionalRule: updateConditionalRule,
|
|
533
|
-
currentView: view,
|
|
534
|
-
onViewChange,
|
|
535
|
-
lifecycleTabLabel: "Team",
|
|
536
|
-
boardGroupByColumnOptions: [...TEAM_BOARD_GROUP_OPTIONS],
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
const tableProps = {
|
|
540
|
-
data: members,
|
|
541
|
-
columns,
|
|
542
|
-
getRowId: (row: TeamMember) => row.id,
|
|
543
|
-
getRowSelectionLabel: (row: TeamMember) => row.name,
|
|
544
|
-
selectable: true,
|
|
545
|
-
searchable: displayOptions.showToolbarSearch,
|
|
546
|
-
showColumnHeaders: displayOptions.showColumnLabels,
|
|
547
|
-
groupable: true,
|
|
548
|
-
defaultSort: { key: "name", dir: "asc" as const },
|
|
549
|
-
emptyState: <p className="text-sm text-muted-foreground">No team members.</p>,
|
|
550
|
-
conditionalRules,
|
|
551
|
-
state: tableState,
|
|
552
|
-
toolbarSlot: (s: ReturnType<typeof useTableState<TeamMember>>) => (
|
|
553
|
-
<TablePropertiesDrawerButton {...drawerToolbarProps} state={s} />
|
|
554
|
-
),
|
|
555
|
-
bulkActionsSlot: (selected: Set<string | number>) => {
|
|
556
|
-
const n = selected.size
|
|
557
|
-
if (n === 0) return null
|
|
558
|
-
return (
|
|
559
|
-
<>
|
|
560
|
-
<span className="sr-only">{n} selected</span>
|
|
561
|
-
<Tip label="Export selection (demo)">
|
|
562
|
-
<Button size="sm" variant="outline" type="button">
|
|
563
|
-
<i className="fa-light fa-arrow-down-to-line" aria-hidden="true" />
|
|
564
|
-
Export
|
|
565
|
-
</Button>
|
|
566
|
-
</Tip>
|
|
567
|
-
</>
|
|
568
|
-
)
|
|
569
|
-
},
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
if (view === "table") {
|
|
573
|
-
return (
|
|
574
|
-
<div className="pb-6">
|
|
575
|
-
<DataTable<TeamMember> {...tableProps} />
|
|
576
|
-
</div>
|
|
577
|
-
)
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
const sharedToolbar = (
|
|
581
|
-
<DataTableToolbar
|
|
582
|
-
state={tableState}
|
|
583
|
-
columns={columns}
|
|
584
|
-
searchable={displayOptions.showToolbarSearch}
|
|
585
|
-
searchAriaLabel="Search team members"
|
|
586
|
-
toolbarSlot={s => <TablePropertiesDrawerButton {...drawerToolbarProps} state={s} />}
|
|
587
|
-
/>
|
|
588
|
-
)
|
|
589
|
-
|
|
590
|
-
if (view === "list") {
|
|
591
|
-
return (
|
|
592
|
-
<div className="flex min-h-0 flex-1 flex-col">
|
|
593
|
-
{sharedToolbar}
|
|
594
|
-
<TeamListView members={tableState.rows} onRowActivate={m => tableState.toggleRow(m.id)} />
|
|
595
|
-
</div>
|
|
596
|
-
)
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
if (view === "board") {
|
|
600
|
-
return (
|
|
601
|
-
<div className="flex min-h-0 flex-1 flex-col">
|
|
602
|
-
{sharedToolbar}
|
|
603
|
-
<TeamBoardView
|
|
604
|
-
members={tableState.rows}
|
|
605
|
-
groupByColumnKey={teamBoardGroupKey}
|
|
606
|
-
onRowActivate={m => tableState.toggleRow(m.id)}
|
|
607
|
-
/>
|
|
608
|
-
</div>
|
|
609
|
-
)
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
if (view === "panel") {
|
|
613
|
-
return (
|
|
614
|
-
<div className="flex min-h-0 flex-1 flex-col">
|
|
615
|
-
{sharedToolbar}
|
|
616
|
-
<ListPageSplitHubChrome aria-label="Team members panel view">
|
|
617
|
-
<FinderPanelView<TeamMember>
|
|
618
|
-
embedded
|
|
619
|
-
groupsColumnTitle="Status"
|
|
620
|
-
groups={teamPanelFinderGroups}
|
|
621
|
-
rows={tableState.rows}
|
|
622
|
-
getRowId={r => r.id}
|
|
623
|
-
getRowGroupId={r => r.status}
|
|
624
|
-
defaultGroupId="all"
|
|
625
|
-
autoSaveId="team-panel-view"
|
|
626
|
-
ariaLabel="Team members panel view"
|
|
627
|
-
emptyList={<p>No team members found</p>}
|
|
628
|
-
renderListRow={(member, isSelected) => (
|
|
629
|
-
<TeamFinderListRow member={member} isSelected={isSelected} />
|
|
630
|
-
)}
|
|
631
|
-
renderDetail={member => (
|
|
632
|
-
<TeamFinderDetail member={member} />
|
|
633
|
-
)}
|
|
634
|
-
/>
|
|
635
|
-
</ListPageSplitHubChrome>
|
|
636
|
-
</div>
|
|
637
|
-
)
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
return (
|
|
641
|
-
<div className="flex min-h-0 flex-1 flex-col">
|
|
642
|
-
<CoachMark state={dashboardCustomizeCoach} />
|
|
643
|
-
{!teamDashboardLayoutEdit ? (
|
|
644
|
-
<DataTableToolbar
|
|
645
|
-
state={tableState}
|
|
646
|
-
columns={columns}
|
|
647
|
-
searchable={displayOptions.showToolbarSearch}
|
|
648
|
-
searchAriaLabel="Search team members"
|
|
649
|
-
toolbarSlot={s => (
|
|
650
|
-
<TablePropertiesDrawerButton
|
|
651
|
-
{...drawerToolbarProps}
|
|
652
|
-
state={s}
|
|
653
|
-
extraActions={
|
|
654
|
-
<Tip side="bottom" label="Edit dashboard layout on canvas">
|
|
655
|
-
<Button
|
|
656
|
-
type="button"
|
|
657
|
-
variant="ghost"
|
|
658
|
-
size="icon-sm"
|
|
659
|
-
aria-label="Edit dashboard layout"
|
|
660
|
-
onClick={handleTeamDashboardLayoutEditStart}
|
|
661
|
-
className="text-muted-foreground hover:text-interactive-hover-foreground hover:bg-interactive-hover"
|
|
662
|
-
>
|
|
663
|
-
<i className="fa-light fa-pen-ruler text-[13px]" aria-hidden="true" />
|
|
664
|
-
</Button>
|
|
665
|
-
</Tip>
|
|
666
|
-
}
|
|
667
|
-
/>
|
|
668
|
-
)}
|
|
669
|
-
/>
|
|
670
|
-
) : null}
|
|
671
|
-
<TeamDashboardChartsSection
|
|
672
|
-
members={tableState.rows as TeamMember[]}
|
|
673
|
-
keyMetrics={dashboardKpi}
|
|
674
|
-
visibleCards={visibleTeamCards}
|
|
675
|
-
cardOrder={teamCardOrder}
|
|
676
|
-
cardSpans={teamCardSpans}
|
|
677
|
-
cardChartTypes={teamCardChartTypes}
|
|
678
|
-
keyMetricsKpiCount={teamKeyMetricsKpiCount}
|
|
679
|
-
layoutEditMode={teamDashboardLayoutEdit}
|
|
680
|
-
onVisibleChange={handleTeamVisibleChange}
|
|
681
|
-
onOrderChange={handleTeamOrderChange}
|
|
682
|
-
onSpanChange={handleTeamSpanChange}
|
|
683
|
-
onChartTypeChange={handleTeamChartTypeChange}
|
|
684
|
-
onKeyMetricsKpiCountChange={setTeamKeyMetricsKpiCount}
|
|
685
|
-
onResetLayout={handleResetTeamDashboardLayout}
|
|
686
|
-
onLayoutEditDone={handleTeamDashboardLayoutEditDone}
|
|
687
|
-
onLayoutEditCancel={handleTeamDashboardLayoutEditCancel}
|
|
688
|
-
/>
|
|
689
|
-
</div>
|
|
690
|
-
)
|
|
691
|
-
})
|
|
692
|
-
|
|
693
|
-
TeamTable.displayName = "TeamTable"
|